Compare commits
127 Commits
v0.10.3-et
...
v0.10.3-et
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8680dbc268 | ||
![]() |
ea2b84c5dc | ||
![]() |
45c7027d3c | ||
![]() |
62017d4f4e | ||
![]() |
0d7dcdc284 | ||
![]() |
0eb3b77bc5 | ||
![]() |
e2fba4bbdd | ||
![]() |
6425a6bfc4 | ||
![]() |
42925e8a7c | ||
![]() |
75e89fe628 | ||
![]() |
f9c806d292 | ||
![]() |
3f5022d515 | ||
![]() |
0748f98d47 | ||
![]() |
3c8fd351a1 | ||
![]() |
40e6d80c35 | ||
![]() |
243cc40da4 | ||
![]() |
3bcc51d12c | ||
![]() |
2afd7d6737 | ||
![]() |
2357d63120 | ||
![]() |
e28d07ebd3 | ||
![]() |
33f960579c | ||
![]() |
6e14bd7959 | ||
![]() |
bdbc0df95b | ||
![]() |
5e10d94e5f | ||
![]() |
a934942bf6 | ||
![]() |
c440e88806 | ||
![]() |
45b7ec005b | ||
![]() |
c748523dbc | ||
![]() |
34eea8dff4 | ||
![]() |
87408c0e6d | ||
![]() |
5ad787075c | ||
![]() |
01ae5a411f | ||
![]() |
cde60a2aba | ||
![]() |
3f5808c67b | ||
![]() |
2c697b40dd | ||
![]() |
9453490bca | ||
![]() |
0baf6ad94d | ||
![]() |
df911c9e97 | ||
![]() |
2a5b59002e | ||
![]() |
44d801a2f5 | ||
![]() |
5ed5a88225 | ||
![]() |
1ca029fe94 | ||
![]() |
ddf3298b41 | ||
![]() |
e66c321ef9 | ||
![]() |
c215aa2f25 | ||
![]() |
e1afcd2678 | ||
![]() |
acd0d6d848 | ||
![]() |
9491c3ab14 | ||
![]() |
2e52d92fce | ||
![]() |
a360e38b7f | ||
![]() |
b72ace5adc | ||
![]() |
c56bd76c73 | ||
![]() |
28e4ea0e76 | ||
![]() |
b9de3044be | ||
![]() |
7df67b99a6 | ||
![]() |
f17a245839 | ||
![]() |
1e1b1b0e80 | ||
![]() |
0742c64c61 | ||
![]() |
992b3cd266 | ||
![]() |
f9a95882e9 | ||
![]() |
7ca78f9fc5 | ||
![]() |
95950dae86 | ||
![]() |
8e83204b34 | ||
![]() |
328c6b499d | ||
![]() |
af708f5d13 | ||
![]() |
7629e4b090 | ||
![]() |
c596d38d7a | ||
![]() |
c643bdcfce | ||
![]() |
630286a781 | ||
![]() |
71e90c12a7 | ||
![]() |
db6594f11f | ||
![]() |
7844987fe0 | ||
![]() |
9f2293f9c4 | ||
![]() |
fd4bf7ce5c | ||
![]() |
0dc969ce2c | ||
![]() |
f5455ef667 | ||
![]() |
edb8028162 | ||
![]() |
f415d637e9 | ||
![]() |
2c9103c769 | ||
![]() |
1c6f91d3a9 | ||
![]() |
05826e2ae1 | ||
![]() |
f6ebd71ac2 | ||
![]() |
49d0157c81 | ||
![]() |
ccfc5e4478 | ||
![]() |
66953dba3d | ||
![]() |
f4df8c070c | ||
![]() |
c3fa4daddb | ||
![]() |
b6782c64f7 | ||
![]() |
8a65621b35 | ||
![]() |
b8b0d46070 | ||
![]() |
2f73970e47 | ||
![]() |
ceacddf9c6 | ||
![]() |
4e89ad5f56 | ||
![]() |
1d4b4f97da | ||
![]() |
ac36a93203 | ||
![]() |
1dd0b242e2 | ||
![]() |
608d9fa9d8 | ||
![]() |
f5841c6a30 | ||
![]() |
266b13ce0b | ||
![]() |
1be9171b93 | ||
![]() |
037c2acee2 | ||
![]() |
38a0c9a9fc | ||
![]() |
8703cff7bf | ||
![]() |
687e3364b0 | ||
![]() |
149f5737dd | ||
![]() |
c2867e44c6 | ||
![]() |
f58562cd5b | ||
![]() |
92845a8640 | ||
![]() |
4fd18007de | ||
![]() |
005be8eec5 | ||
![]() |
402457640c | ||
![]() |
c3dd0facd2 | ||
![]() |
2fa5600d24 | ||
![]() |
c35416ec13 | ||
![]() |
9e6e31a728 | ||
![]() |
d7677af085 | ||
![]() |
dc0ba15828 | ||
![]() |
6527565a18 | ||
![]() |
c0a4004fd3 | ||
![]() |
97384d1ebb | ||
![]() |
41768eb157 | ||
![]() |
0c89493b58 | ||
![]() |
d5d1be794d | ||
![]() |
b2dc623448 | ||
![]() |
fa90a82745 | ||
![]() |
b55532e93e | ||
![]() |
880270bfe6 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/testdata
|
11
Dockerfile.build
Normal file
11
Dockerfile.build
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM node:lts AS builder
|
||||||
|
ARG BASE_PATH=./
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . /src
|
||||||
|
RUN yarn config set enableTelemetry 0 && \
|
||||||
|
yarn install --immutable --network-timeout=300000 && \
|
||||||
|
yarn build --base=$BASE_PATH
|
||||||
|
|
||||||
|
FROM ghcr.io/static-web-server/static-web-server:2
|
||||||
|
ENV SERVER_ROOT=/app
|
||||||
|
COPY --from=builder /src/dist /app
|
41
README.md
41
README.md
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
* [Fork differences](#fork-differences)
|
* [Fork differences](#fork-differences)
|
||||||
* [Changes](#changes)
|
* [Changes](#changes)
|
||||||
|
* [exclusive for etke.cc customers](#exclusive-for-etkecc-customers)
|
||||||
* [Development](#development)
|
* [Development](#development)
|
||||||
* [Support](#support)
|
* [Support](#support)
|
||||||
* [Configuration](#configuration)
|
* [Configuration](#configuration)
|
||||||
@@ -58,6 +59,8 @@ The full list is described below in the [Changes](#changes) section.
|
|||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
|
_the list will be updated as new changes are added_
|
||||||
|
|
||||||
The following changes are already implemented:
|
The following changes are already implemented:
|
||||||
|
|
||||||
* 🛑 [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
|
* 🛑 [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
|
||||||
@@ -97,21 +100,31 @@ The following changes are already implemented:
|
|||||||
* 🛑 [Prevent accidental user overwrites](https://github.com/etkecc/synapse-admin/pull/139)
|
* 🛑 [Prevent accidental user overwrites](https://github.com/etkecc/synapse-admin/pull/139)
|
||||||
* 🔍 [Allow providing login form details via GET params](https://github.com/etkecc/synapse-admin/pull/140)
|
* 🔍 [Allow providing login form details via GET params](https://github.com/etkecc/synapse-admin/pull/140)
|
||||||
* 🎨 [Add preferred theme colors to login page and footer](https://github.com/etkecc/synapse-admin/pull/155)
|
* 🎨 [Add preferred theme colors to login page and footer](https://github.com/etkecc/synapse-admin/pull/155)
|
||||||
* 🔰 [Add "Assign Admin" button to the rooms](https://github.com/etkecc/synapse-admin/pull/156)
|
* 🛡️ [Add "Assign Admin" button to the rooms](https://github.com/etkecc/synapse-admin/pull/156)
|
||||||
* 🖼️ [Add rooms' avatars](https://github.com/etkecc/synapse-admin/pull/158)
|
* 🖼️ [Add rooms' avatars](https://github.com/etkecc/synapse-admin/pull/158)
|
||||||
* 🤖 [User Badges](https://github.com/etkecc/synapse-admin/pull/160)
|
* 🏷️ [User Badges](https://github.com/etkecc/synapse-admin/pull/160)
|
||||||
* 🔑 [Allow prefilling any fields on the login form via GET params](https://github.com/etkecc/synapse-admin/pull/181)
|
* 🔄 [Allow prefilling any fields on the login form via GET params](https://github.com/etkecc/synapse-admin/pull/181)
|
||||||
* _(for [etke.cc](https://etke.cc) customers only)_ [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182)
|
* 🎞️ [Add "Media" tab for rooms](https://github.com/etkecc/synapse-admin/pull/196)
|
||||||
|
* 📞 [Support E.164-based Matrix IDs (MSC4009)](https://github.com/etkecc/synapse-admin/pull/214)
|
||||||
|
* 🛑 [Add support for Account Suspension (MSC3823)](https://github.com/etkecc/synapse-admin/pull/195)
|
||||||
|
* 🗑️ [Add "Purge Remote Media" button](https://github.com/etkecc/synapse-admin/pull/237)
|
||||||
|
* [Respect base url (`BASE_PATH` / `vite build --base`) when loading `config.json`](https://github.com/etkecc/synapse-admin/pull/274)
|
||||||
|
* [Add Users' Account Data tab](https://github.com/etkecc/synapse-admin/pull/276)
|
||||||
|
|
||||||
|
#### exclusive for [etke.cc](https://etke.cc) customers
|
||||||
|
|
||||||
_the list will be updated as new changes are added_
|
We at [etke.cc](https://etke.cc) attempting to develop everything open-source, but some things are too specific to be used by anyone else.
|
||||||
|
The following list contains such features - they are only available for [etke.cc](https://etke.cc) customers.
|
||||||
|
|
||||||
|
* 📊 [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182)
|
||||||
|
* 📬 [Server Notifications indicator and page](https://github.com/etkecc/synapse-admin/pull/240)
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
`just run-dev` to start the development stack (depending on your system speed, you may want to re-run this command if
|
`just run-dev` to start the development stack (depending on your system speed, you may want to re-run this command if
|
||||||
user creation fails)
|
user creation fails)
|
||||||
|
|
||||||
This command initializes the development environment (local Synapse server and Postgres DB),
|
This command initializes the development environment (local Synapse server, Element Web client app, and Postgres DB),
|
||||||
and launches the app in a dev mode at `http://localhost:5173`
|
and launches the app in a dev mode at `http://localhost:5173`
|
||||||
|
|
||||||
After that open [http://localhost:5173](http://localhost:5173?username=admin&password=admin&server=http://localhost:8008) in your browser,
|
After that open [http://localhost:5173](http://localhost:5173?username=admin&password=admin&server=http://localhost:8008) in your browser,
|
||||||
@@ -121,6 +134,8 @@ login using the following credentials:
|
|||||||
* Password: admin
|
* Password: admin
|
||||||
* Homeserver URL: http://localhost:8008
|
* Homeserver URL: http://localhost:8008
|
||||||
|
|
||||||
|
Element Web runs on http://localhost:8080
|
||||||
|
|
||||||
### Support
|
### Support
|
||||||
|
|
||||||
If you have any questions or need help, feel free to join the [community room](https://matrix.to/#/#synapse-admin:etke.cc) or create an issue on GitHub.
|
If you have any questions or need help, feel free to join the [community room](https://matrix.to/#/#synapse-admin:etke.cc) or create an issue on GitHub.
|
||||||
@@ -246,6 +261,7 @@ You have three options:
|
|||||||
hostname: synapse-admin
|
hostname: synapse-admin
|
||||||
build:
|
build:
|
||||||
context: https://github.com/etkecc/synapse-admin.git
|
context: https://github.com/etkecc/synapse-admin.git
|
||||||
|
dockerfile: Dockerfile.build
|
||||||
args:
|
args:
|
||||||
- BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
- BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||||
# - NODE_OPTIONS="--max_old_space_size=1024"
|
# - NODE_OPTIONS="--max_old_space_size=1024"
|
||||||
@@ -274,7 +290,7 @@ Example for Traefik:
|
|||||||
```yml
|
```yml
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:mimolette
|
image: traefik:v3
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
@@ -287,11 +303,12 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.synapse-admin.rule=Host(`example.com`)&&PathPrefix(`/admin`)"
|
- "traefik.http.routers.admin.rule=Host(`example.com`) && PathPrefix(`/admin`)"
|
||||||
- "traefik.http.routers.synapse-admin.middlewares=admin,admin_path"
|
- "traefik.http.services.admin.loadbalancer.server.port=80"
|
||||||
- "traefik.http.middlewares.admin.redirectregex.regex=^(.*)/admin/?"
|
- "traefik.http.middlewares.admin-slashless-redirect.redirectregex.regex=(/admin)$$"
|
||||||
- "traefik.http.middlewares.admin.redirectregex.replacement=$${1}/admin/"
|
- "traefik.http.middlewares.admin-slashless-redirect.redirectregex.replacement=$${1}/"
|
||||||
- "traefik.http.middlewares.admin_path.stripprefix.prefixes=/admin"
|
- "traefik.http.middlewares.admin-strip-prefix.stripprefix.prefixes=/admin"
|
||||||
|
- "traefik.http.routers.admin.middlewares=admin-slashless-redirect,admin-strip-prefix"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
synapse:
|
synapse:
|
||||||
image: ghcr.io/element-hq/synapse:latest
|
image: ghcr.io/element-hq/synapse:develop
|
||||||
entrypoint: python
|
entrypoint: python
|
||||||
command: "-m synapse.app.homeserver -c /config/homeserver.yaml"
|
command: "-m synapse.app.homeserver -c /config/homeserver.yaml"
|
||||||
ports:
|
ports:
|
||||||
@@ -18,3 +18,17 @@ services:
|
|||||||
POSTGRES_PASSWORD: synapse
|
POSTGRES_PASSWORD: synapse
|
||||||
POSTGRES_DB: synapse
|
POSTGRES_DB: synapse
|
||||||
POSTGRES_INITDB_ARGS: "--lc-collate C --lc-ctype C --encoding UTF8"
|
POSTGRES_INITDB_ARGS: "--lc-collate C --lc-ctype C --encoding UTF8"
|
||||||
|
|
||||||
|
element:
|
||||||
|
image: docker.io/vectorim/element-web:latest
|
||||||
|
depends_on:
|
||||||
|
synapse:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: true
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./testdata/element/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- /dev/null:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
- ./testdata/element/config.json:/app/config.json:ro
|
||||||
|
|
||||||
|
@@ -5,6 +5,7 @@ services:
|
|||||||
image: ghcr.io/etkecc/synapse-admin:latest
|
image: ghcr.io/etkecc/synapse-admin:latest
|
||||||
# build:
|
# build:
|
||||||
# context: .
|
# context: .
|
||||||
|
# dockerfile: Dockerfile.build
|
||||||
|
|
||||||
# to use the docker-compose as standalone without a local repo clone,
|
# to use the docker-compose as standalone without a local repo clone,
|
||||||
# replace the context definition with this:
|
# replace the context definition with this:
|
||||||
|
@@ -2,12 +2,30 @@ 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/",
|
||||||
coverageReporters: ["html", "text", "text-summary", "cobertura"],
|
coverageReporters: ["html", "text", "text-summary", "cobertura"],
|
||||||
extensionsToTreatAsEsm: [".ts", ".tsx"],
|
extensionsToTreatAsEsm: [".ts", ".tsx"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"],
|
setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.tsx?$': [
|
||||||
|
'ts-jest',
|
||||||
|
{
|
||||||
|
diagnostics: {
|
||||||
|
ignoreCodes: [1343]
|
||||||
|
},
|
||||||
|
astTransformers: {
|
||||||
|
before: [
|
||||||
|
{
|
||||||
|
path: 'ts-jest-mock-import-meta',
|
||||||
|
options: { metaObjectReplacement: { env: { BASE_URL: "/" } } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
2
justfile
2
justfile
@@ -16,6 +16,8 @@ run-dev:
|
|||||||
@docker-compose -f docker-compose-dev.yml up -d postgres
|
@docker-compose -f docker-compose-dev.yml up -d postgres
|
||||||
@echo "Starting Synapse..."
|
@echo "Starting Synapse..."
|
||||||
@docker-compose -f docker-compose-dev.yml up -d synapse
|
@docker-compose -f docker-compose-dev.yml up -d synapse
|
||||||
|
@echo "Starting Element Web..."
|
||||||
|
@docker-compose -f docker-compose-dev.yml up -d element
|
||||||
@echo "Ensure admin user is registered..."
|
@echo "Ensure admin user is registered..."
|
||||||
@docker-compose -f docker-compose-dev.yml exec synapse register_new_matrix_user --admin -u admin -p admin -c /config/homeserver.yaml http://localhost:8008 || true
|
@docker-compose -f docker-compose-dev.yml exec synapse register_new_matrix_user --admin -u admin -p admin -c /config/homeserver.yaml http://localhost:8008 || true
|
||||||
@echo "Starting the app..."
|
@echo "Starting the app..."
|
||||||
|
62
package.json
62
package.json
@@ -11,64 +11,64 @@
|
|||||||
"url": "https://github.com/etkecc/synapse-admin"
|
"url": "https://github.com/etkecc/synapse-admin"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.13.0",
|
"@eslint/js": "^9.19.0",
|
||||||
"@testing-library/dom": "^10.0.0",
|
"@testing-library/dom": "^10.0.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/lodash": "^4.17.13",
|
"@types/lodash": "^4.17.14",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.10",
|
||||||
"@types/papaparse": "^5.3.15",
|
"@types/papaparse": "^5.3.15",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.14.0",
|
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
||||||
"@typescript-eslint/parser": "^8.15.0",
|
"@typescript-eslint/parser": "^8.19.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.19.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.3",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"eslint-plugin-yaml": "^1.0.3",
|
"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.4.1",
|
"prettier": "^3.4.2",
|
||||||
"react-test-renderer": "^18.3.1",
|
"react-test-renderer": "^18.3.1",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.16.0",
|
"typescript-eslint": "^8.21.0",
|
||||||
"vite": "^5.4.11",
|
"vite": "^6.0.11",
|
||||||
"vite-plugin-version-mark": "^0.1.3"
|
"vite-plugin-version-mark": "^0.1.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.5",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.13.5",
|
"@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": "^6.1.8",
|
"@mui/icons-material": "^6.3.1",
|
||||||
"@mui/material": "^6.1.9",
|
"@mui/material": "^6.4.0",
|
||||||
"@mui/utils": "^5.16.8",
|
"@mui/utils": "^5.16.14",
|
||||||
"@tanstack/react-query": "^5.62.0",
|
"@tanstack/react-query": "^5.64.2",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
|
"jest-fixed-jsdom": "^0.0.9",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.5.1",
|
||||||
"ra-core": "^5.3.4",
|
"ra-core": "^5.4.4",
|
||||||
"ra-i18n-polyglot": "^5.3.4",
|
"ra-i18n-polyglot": "^5.4.4",
|
||||||
"ra-language-english": "^5.3.4",
|
"ra-language-english": "^5.4.4",
|
||||||
"ra-language-farsi": "^5.0.0",
|
"ra-language-farsi": "^5.1.0",
|
||||||
"ra-language-french": "^5.4.0",
|
"ra-language-french": "^5.5.2",
|
||||||
"ra-language-italian": "^3.13.1",
|
"ra-language-italian": "^3.13.1",
|
||||||
"ra-language-russian": "^4.14.2",
|
"ra-language-russian": "^4.14.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-admin": "^5.4.0",
|
"react-admin": "^5.5.2",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-is": "^18.3.1",
|
"react-is": "^18.3.1",
|
||||||
"react-router": "^6.26.2",
|
"ts-jest-mock-import-meta": "^1.2.1"
|
||||||
"react-router-dom": "^6.28.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite serve",
|
"start": "vite serve",
|
||||||
|
BIN
screenshots/etke.cc/server-notifications/badge.webp
Normal file
BIN
screenshots/etke.cc/server-notifications/badge.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
BIN
screenshots/etke.cc/server-notifications/page.webp
Normal file
BIN
screenshots/etke.cc/server-notifications/page.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
@@ -1,12 +1,32 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import fetchMock from "jest-fetch-mock";
|
import fetchMock from "jest-fetch-mock";
|
||||||
fetchMock.enableMocks();
|
fetchMock.enableMocks();
|
||||||
|
|
||||||
|
jest.mock("./synapse/authProvider", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
logout: jest.fn().mockResolvedValue(undefined),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
describe("App", () => {
|
describe("App", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset all mocks before each test
|
||||||
|
fetchMock.resetMocks();
|
||||||
|
// Mock any fetch call to return empty JSON immediately
|
||||||
|
fetchMock.mockResponseOnce(JSON.stringify({}));
|
||||||
|
});
|
||||||
|
|
||||||
it("renders", async () => {
|
it("renders", async () => {
|
||||||
render(<App />);
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
|
||||||
await screen.findAllByText("Welcome to Synapse Admin");
|
await screen.findAllByText("Welcome to Synapse Admin");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
11
src/App.tsx
11
src/App.tsx
@@ -2,7 +2,7 @@ import { merge } from "lodash";
|
|||||||
import polyglotI18nProvider from "ra-i18n-polyglot";
|
import polyglotI18nProvider from "ra-i18n-polyglot";
|
||||||
|
|
||||||
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
|
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
|
||||||
import { createContext, useContext } from "react";
|
|
||||||
import { Route } from "react-router-dom";
|
import { Route } from "react-router-dom";
|
||||||
|
|
||||||
import AdminLayout from "./components/AdminLayout";
|
import AdminLayout from "./components/AdminLayout";
|
||||||
@@ -22,10 +22,10 @@ import rooms from "./resources/rooms";
|
|||||||
import userMediaStats from "./resources/user_media_statistics";
|
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, { ServerStatusResponse } from "./synapse/dataProvider";
|
import dataProvider from "./synapse/dataProvider";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { Config } from "./utils/config";
|
|
||||||
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
||||||
|
import ServerNotificationsPage from "./components/etke.cc/ServerNotificationsPage";
|
||||||
|
|
||||||
// TODO: Can we use lazy loading together with browser locale?
|
// TODO: Can we use lazy loading together with browser locale?
|
||||||
const messages = {
|
const messages = {
|
||||||
@@ -66,6 +66,7 @@ export const App = () => (
|
|||||||
<CustomRoutes>
|
<CustomRoutes>
|
||||||
<Route path="/import_users" element={<UserImport />} />
|
<Route path="/import_users" element={<UserImport />} />
|
||||||
<Route path="/server_status" element={<ServerStatusPage />} />
|
<Route path="/server_status" element={<ServerStatusPage />} />
|
||||||
|
<Route path="/server_notifications" element={<ServerNotificationsPage />} />
|
||||||
</CustomRoutes>
|
</CustomRoutes>
|
||||||
<Resource {...users} />
|
<Resource {...users} />
|
||||||
<Resource {...rooms} />
|
<Resource {...rooms} />
|
||||||
@@ -88,8 +89,4 @@ export const App = () => (
|
|||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AppContext = createContext({});
|
|
||||||
|
|
||||||
export const useAppContext = () => useContext(AppContext) as Config;
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
7
src/Context.tsx
Normal file
7
src/Context.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
import { Config } from "./utils/config";
|
||||||
|
|
||||||
|
export const AppContext = createContext<Config>({} as Config);
|
||||||
|
|
||||||
|
export const useAppContext = () => useContext(AppContext) as Config;
|
@@ -5,6 +5,7 @@ import { Icons, DefaultIcon } from "../utils/icons";
|
|||||||
import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
|
import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
|
import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
|
||||||
|
import { ServerNotificationsBadge } from "./etke.cc/ServerNotificationsBadge";
|
||||||
|
|
||||||
const AdminUserMenu = () => {
|
const AdminUserMenu = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -50,6 +51,7 @@ const AdminAppBar = () => {
|
|||||||
return (<AppBar userMenu={<AdminUserMenu />}>
|
return (<AppBar userMenu={<AdminUserMenu />}>
|
||||||
<TitlePortal />
|
<TitlePortal />
|
||||||
<ServerStatusBadge />
|
<ServerStatusBadge />
|
||||||
|
<ServerNotificationsBadge />
|
||||||
<InspectorButton />
|
<InspectorButton />
|
||||||
</AppBar>);
|
</AppBar>);
|
||||||
};
|
};
|
||||||
@@ -61,7 +63,7 @@ const AdminMenu = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Menu {...props}>
|
<Menu {...props}>
|
||||||
<Menu.ResourceItems />
|
<Menu.ResourceItems />
|
||||||
{menu.map((item, index) => {
|
{menu && menu.map((item, index) => {
|
||||||
const { url, icon, label } = item;
|
const { url, icon, label } = item;
|
||||||
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
|
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
|
||||||
|
|
||||||
|
65
src/components/UserAccountData.tsx
Normal file
65
src/components/UserAccountData.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useDataProvider, useRecordContext, useTranslate } from "react-admin";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Typography, Box, Stack, Accordion, AccordionSummary, AccordionDetails } from "@mui/material";
|
||||||
|
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||||
|
import { SynapseDataProvider } from "../synapse/dataProvider";
|
||||||
|
|
||||||
|
const UserAccountData = () => {
|
||||||
|
const dataProvider = useDataProvider() as SynapseDataProvider;
|
||||||
|
const record = useRecordContext();
|
||||||
|
const translate = useTranslate();
|
||||||
|
const [globalAccountData, setGlobalAccountData] = useState({});
|
||||||
|
const [roomsAccountData, setRoomsAccountData] = useState({});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAccountData = async () => {
|
||||||
|
const accountData = await dataProvider.getAccountData(record.id);
|
||||||
|
setGlobalAccountData(accountData.account_data.global);
|
||||||
|
setRoomsAccountData(accountData.account_data.rooms);
|
||||||
|
};
|
||||||
|
fetchAccountData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (Object.keys(globalAccountData).length === 0 && Object.keys(roomsAccountData).length === 0) {
|
||||||
|
return <Typography variant="body2">{translate('ra.navigation.no_results', {
|
||||||
|
resource: 'Account Data',
|
||||||
|
_: 'No results found.',
|
||||||
|
})}</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
spacing={2}
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<Typography variant="h6">{translate('resources.users.account_data.title')}</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
<Box>
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary expandIcon={<ArrowDownwardIcon />}>
|
||||||
|
<Typography variant="h6">{translate('resources.users.account_data.global')}</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(globalAccountData, null, 4)}</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary expandIcon={<ArrowDownwardIcon />}>
|
||||||
|
<Typography variant="h6">{translate('resources.users.account_data.rooms')}</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(roomsAccountData, null, 4)}</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserAccountData;
|
@@ -28,3 +28,15 @@ Server Status page. This page contains the following information:
|
|||||||
* Overall server status (up/updating/has issues)
|
* Overall server status (up/updating/has issues)
|
||||||
* Details about the currently running command (if any)
|
* Details about the currently running command (if any)
|
||||||
* Details about the server's components statuses (up/down with error details and suggested actions) by categories
|
* Details about the server's components statuses (up/down with error details and suggested actions) by categories
|
||||||
|
|
||||||
|
### Server Notifications icon
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In the application bar the new notifications icon is displayed that shows the number of unread (not removed) notifications
|
||||||
|
|
||||||
|
### Server Notifications page
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When you click on a notification from the [Server Notifications icon](#server-notifications-icon)'s list in the application bar, you will be redirected to the Server Notifications page. This page contains the full text of all the notifications you have about your server.
|
||||||
|
184
src/components/etke.cc/ServerNotificationsBadge.tsx
Normal file
184
src/components/etke.cc/ServerNotificationsBadge.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import { Badge, useTheme, Button, Paper, Popper, ClickAwayListener, Box, List, ListItem, ListItemText, Typography, ListSubheader, IconButton, Divider, Tooltip } from "@mui/material";
|
||||||
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import { useDataProvider, useStore } from "react-admin";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import { Fragment, useEffect, useState } from "react";
|
||||||
|
import { useAppContext } from "../../Context";
|
||||||
|
import { ServerNotificationsResponse } from "../../synapse/dataProvider";
|
||||||
|
|
||||||
|
const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000;
|
||||||
|
|
||||||
|
const useServerNotifications = () => {
|
||||||
|
const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", { notifications: [], success: false });
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
const { notifications, success } = serverNotifications;
|
||||||
|
|
||||||
|
const fetchNotifications = async () => {
|
||||||
|
const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(etkeccAdmin);
|
||||||
|
setServerNotifications({
|
||||||
|
...notificationsResponse,
|
||||||
|
notifications: notificationsResponse.notifications,
|
||||||
|
success: notificationsResponse.success
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteServerNotifications = async () => {
|
||||||
|
const deleteResponse = await dataProvider.deleteServerNotifications(etkeccAdmin);
|
||||||
|
if (deleteResponse.success) {
|
||||||
|
await fetchNotifications();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let serverNotificationsInterval: NodeJS.Timeout;
|
||||||
|
if (etkeccAdmin) {
|
||||||
|
fetchNotifications();
|
||||||
|
setTimeout(() => {
|
||||||
|
// start the interval after the SERVER_NOTIFICATIONS_INTERVAL_TIME to avoid too many requests
|
||||||
|
serverNotificationsInterval = setInterval(fetchNotifications, SERVER_NOTIFICATIONS_INTERVAL_TIME);
|
||||||
|
}, SERVER_NOTIFICATIONS_INTERVAL_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (serverNotificationsInterval) {
|
||||||
|
clearInterval(serverNotificationsInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [etkeccAdmin]);
|
||||||
|
|
||||||
|
return { success, notifications, deleteServerNotifications };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ServerNotificationsBadge = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { success, notifications, deleteServerNotifications } = useServerNotifications();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
// Modify menu state to work with Popper
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
|
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorEl(anchorEl ? null : event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSeeAllNotifications = () => {
|
||||||
|
handleClose();
|
||||||
|
navigate("/server_notifications");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAllNotifications = async () => {
|
||||||
|
deleteServerNotifications()
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<IconButton onClick={handleOpen} sx={{ color: theme.palette.common.white }}>
|
||||||
|
<Tooltip title={notifications && notifications.length > 0 ? `${notifications.length} new notifications` : `No notifications yet`}>
|
||||||
|
{notifications && notifications.length > 0 && (
|
||||||
|
<Badge badgeContent={notifications.length} color="error">
|
||||||
|
<NotificationsIcon />
|
||||||
|
</Badge>
|
||||||
|
) || <NotificationsIcon />}
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
<Popper
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
placement="bottom-end"
|
||||||
|
style={{ zIndex: 1300 }}
|
||||||
|
>
|
||||||
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
maxHeight: "350px",
|
||||||
|
overflowY: "auto",
|
||||||
|
minWidth: "300px",
|
||||||
|
maxWidth: {
|
||||||
|
xs: "100vw", // Full width on mobile
|
||||||
|
sm: "400px" // Fixed width on desktop
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(!notifications || notifications.length === 0) ? (
|
||||||
|
<Typography sx={{ p: 1 }} variant="body2">No new notifications</Typography>
|
||||||
|
) : (
|
||||||
|
<List sx={{ p: 0 }} dense={true}>
|
||||||
|
<ListSubheader
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
fontWeight: "bold",
|
||||||
|
backgroundColor: "inherit",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">Notifications</Typography>
|
||||||
|
<Box sx={{ cursor: "pointer", color: theme.palette.primary.main }} onClick={() => handleSeeAllNotifications()}>See all notifications</Box>
|
||||||
|
</ListSubheader>
|
||||||
|
<Divider />
|
||||||
|
{notifications.map((notification, index) => {
|
||||||
|
return (<Fragment key={notification.event_id ? notification.event_id : index }>
|
||||||
|
<ListItem
|
||||||
|
onClick={() => handleSeeAllNotifications()}
|
||||||
|
sx={{
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "action.hover",
|
||||||
|
cursor: "pointer"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: notification.output.split("\n")[0] }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
</Fragment>
|
||||||
|
)})}
|
||||||
|
<ListItem>
|
||||||
|
<Button
|
||||||
|
key="clear-all-notifications"
|
||||||
|
onClick={() => handleClearAllNotifications()}
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
sx={{
|
||||||
|
pl: 0,
|
||||||
|
pt: 1,
|
||||||
|
verticalAlign: "middle"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
|
||||||
|
Clear all
|
||||||
|
</Button>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Popper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
58
src/components/etke.cc/ServerNotificationsPage.tsx
Normal file
58
src/components/etke.cc/ServerNotificationsPage.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Box, Typography, Paper, Button } from "@mui/material"
|
||||||
|
import { Stack } from "@mui/material"
|
||||||
|
import { useStore } from "react-admin"
|
||||||
|
import dataProvider, { ServerNotificationsResponse } from "../../synapse/dataProvider"
|
||||||
|
import { useAppContext } from "../../Context";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
const DisplayTime = ({ date }: { date: string }) => {
|
||||||
|
const dateFromDateString = new Date(date);
|
||||||
|
return <>{dateFromDateString.toLocaleString()}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServerNotificationsPage = () => {
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", {
|
||||||
|
notifications: [],
|
||||||
|
success: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const notifications = serverNotifications.notifications;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={3} mt={3}>
|
||||||
|
<Stack spacing={1} direction="row" alignItems="center">
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", gap: 1 }}>
|
||||||
|
<Typography variant="h4">Server Notifications</Typography>
|
||||||
|
<Button variant="text" color="error" onClick={async () => {
|
||||||
|
await dataProvider.deleteServerNotifications(etkeccAdmin);
|
||||||
|
setServerNotifications({
|
||||||
|
notifications: [],
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Clear
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{notifications.length === 0 ? (
|
||||||
|
<Paper sx={{ p: 2 }}>
|
||||||
|
<Typography>No new notifications.</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
notifications.map((notification, index) => (
|
||||||
|
<Paper key={notification.event_id ? notification.event_id : index} sx={{ p: 2 }}>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold" color="text.secondary">
|
||||||
|
<DisplayTime date={notification.sent_at} />
|
||||||
|
</Typography>
|
||||||
|
<Typography dangerouslySetInnerHTML={{ __html: notification.output }} />
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerNotificationsPage;
|
@@ -1,6 +1,6 @@
|
|||||||
import { Avatar, Badge, Theme, Tooltip } from "@mui/material";
|
import { Avatar, Badge, Theme, Tooltip } from "@mui/material";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useAppContext } from "../../App";
|
import { useAppContext } from "../../Context";
|
||||||
import { Button, useDataProvider, useStore } from "react-admin";
|
import { Button, useDataProvider, useStore } from "react-admin";
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
|
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
|
||||||
|
@@ -35,6 +35,7 @@ import { useMutation } from "@tanstack/react-query";
|
|||||||
import { dateParser } from "../utils/date";
|
import { dateParser } from "../utils/date";
|
||||||
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
|
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
|
||||||
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
||||||
|
import decodeURLComponent from "../utils/decodeURLComponent";
|
||||||
|
|
||||||
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
@@ -124,6 +125,80 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PurgeRemoteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
const PurgeRemoteMediaToolbar = (props: ToolbarProps) => (
|
||||||
|
<Toolbar {...props}>
|
||||||
|
<SaveButton label="purge_remote_media.action.send" icon={<DeleteSweepIcon />} />
|
||||||
|
<Button label="ra.action.cancel" onClick={onClose}>
|
||||||
|
<IconCancel />
|
||||||
|
</Button>
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose}>
|
||||||
|
<DialogTitle>{translate("purge_remote_media.action.send")}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>{translate("purge_remote_media.helper.send")}</DialogContentText>
|
||||||
|
<SimpleForm toolbar={<PurgeRemoteMediaToolbar />} onSubmit={onSubmit}>
|
||||||
|
<DateTimeInput
|
||||||
|
source="before_ts"
|
||||||
|
label="purge_remote_media.fields.before_ts"
|
||||||
|
defaultValue={0}
|
||||||
|
parse={dateParser}
|
||||||
|
/>
|
||||||
|
</SimpleForm>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PurgeRemoteMediaButton = (props: ButtonProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const notify = useNotify();
|
||||||
|
const dataProvider = useDataProvider<SynapseDataProvider>();
|
||||||
|
const { mutate: purgeRemoteMedia, isPending } = useMutation({
|
||||||
|
mutationFn: (values: DeleteMediaParams) => dataProvider.purgeRemoteMedia(values),
|
||||||
|
onSuccess: () => {
|
||||||
|
notify("purge_remote_media.action.send_success");
|
||||||
|
closeDialog();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
notify("purge_remote_media.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const openDialog = () => setOpen(true);
|
||||||
|
const closeDialog = () => setOpen(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
label="purge_remote_media.action.send"
|
||||||
|
onClick={openDialog}
|
||||||
|
disabled={isPending}
|
||||||
|
sx={{
|
||||||
|
"&:hover": {
|
||||||
|
// Reset on mouse devices
|
||||||
|
"@media (hover: none)": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteSweepIcon />
|
||||||
|
</Button>
|
||||||
|
<PurgeRemoteMediaDialog open={open} onClose={closeDialog} onSubmit={purgeRemoteMedia} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ProtectMediaButton = (props: ButtonProps) => {
|
export const ProtectMediaButton = (props: ButtonProps) => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
@@ -314,6 +389,7 @@ export const QuarantineMediaButton = (props: ButtonProps) => {
|
|||||||
export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
|
export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const notify = useNotify();
|
||||||
const isImage = mimetype && mimetype.startsWith("image/");
|
const isImage = mimetype && mimetype.startsWith("image/");
|
||||||
|
|
||||||
const openFileInNewTab = (blobURL: string) => {
|
const openFileInNewTab = (blobURL: string) => {
|
||||||
@@ -340,6 +416,8 @@ export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
|
|||||||
const handleFile = async (preview: boolean) => {
|
const handleFile = async (preview: boolean) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetchAuthenticatedMedia(mxcURL, "original");
|
const response = await fetchAuthenticatedMedia(mxcURL, "original");
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
const blobURL = URL.createObjectURL(blob);
|
const blobURL = URL.createObjectURL(blob);
|
||||||
if (preview) {
|
if (preview) {
|
||||||
@@ -347,6 +425,17 @@ export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
|
|||||||
} else {
|
} else {
|
||||||
downloadFile(blobURL);
|
downloadFile(blobURL);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const body = await response.json();
|
||||||
|
notify("resources.room_media.action.error", {
|
||||||
|
messageArgs: {
|
||||||
|
errcode: body.errcode,
|
||||||
|
errstatus: response.status,
|
||||||
|
error: body.error,
|
||||||
|
},
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -391,8 +480,16 @@ export const MediaIDField = ({ source }) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadName = decodeURIComponent(get(record, "upload_name")?.toString());
|
let uploadName = mediaID;
|
||||||
const mxcURL = `mxc://${homeserver}/${mediaID}`;
|
if (get(record, "upload_name")) {
|
||||||
|
uploadName = decodeURLComponent(get(record, "upload_name")?.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mxcURL = mediaID;
|
||||||
|
if (!mediaID.startsWith(`mxc://${homeserver}`)) {
|
||||||
|
// this is user's media, where mediaID doesn't have the mxc://home_server/ prefix as it has in the rooms
|
||||||
|
mxcURL = `mxc://${homeserver}/${mediaID}`;
|
||||||
|
}
|
||||||
|
|
||||||
return <ViewMediaButton mxcURL={mxcURL} label={mediaID} uploadName={uploadName} mimetype={record.media_type}/>;
|
return <ViewMediaButton mxcURL={mxcURL} label={mediaID} uploadName={uploadName} mimetype={record.media_type}/>;
|
||||||
};
|
};
|
||||||
@@ -408,7 +505,10 @@ export const ReportMediaContent = ({ source }) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadName = decodeURIComponent(get(record, "event_json.content.body")?.toString());
|
let uploadName = "";
|
||||||
|
if (get(record, "event_json.content.body")) {
|
||||||
|
uploadName = decodeURLComponent(get(record, "event_json.content.body")?.toString());
|
||||||
|
}
|
||||||
|
|
||||||
return <ViewMediaButton mxcURL={mxcURL} label={mxcURL} uploadName={uploadName} mimetype={record.media_type}/>;
|
return <ViewMediaButton mxcURL={mxcURL} label={mxcURL} uploadName={uploadName} mimetype={record.media_type}/>;
|
||||||
};
|
};
|
||||||
|
@@ -15,6 +15,7 @@ const fixedGermanMessages = {
|
|||||||
action: {
|
action: {
|
||||||
...formalGermanMessages.ra.action,
|
...formalGermanMessages.ra.action,
|
||||||
update_application: "Anwendung aktualisieren",
|
update_application: "Anwendung aktualisieren",
|
||||||
|
select_all_button: "Alle auswählen",
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
...formalGermanMessages.ra.page,
|
...formalGermanMessages.ra.page,
|
||||||
@@ -28,6 +29,7 @@ const fixedGermanMessages = {
|
|||||||
"Sie haben nicht die erforderlichen Berechtigungen um auf diese Seite zuzugreifen.",
|
"Sie haben nicht die erforderlichen Berechtigungen um auf diese Seite zuzugreifen.",
|
||||||
authentication_error:
|
authentication_error:
|
||||||
"Der Authentifizierungsserver hat einen Fehler zurückgegeben und Ihre Anmeldedaten konnten nicht überprüft werden.",
|
"Der Authentifizierungsserver hat einen Fehler zurückgegeben und Ihre Anmeldedaten konnten nicht überprüft werden.",
|
||||||
|
select_all_limit_reached: "Es gibt zu viele Elemente, um sie alle auszuwählen. Es wurden nur die ersten %{max} Elemente ausgewählt.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -55,7 +57,7 @@ const de: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
|
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
|
||||||
tabs: { sso: "SSO", experimental: "Experimentell", limits: "Rate Limits" },
|
tabs: { sso: "SSO", experimental: "Experimentell", limits: "Rate Limits", account_data: "Kontodaten" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
details: "Raumdetails",
|
details: "Raumdetails",
|
||||||
@@ -64,6 +66,7 @@ const de: SynapseTranslationMessages = {
|
|||||||
members: "Mitglieder",
|
members: "Mitglieder",
|
||||||
detail: "Details",
|
detail: "Details",
|
||||||
permission: "Berechtigungen",
|
permission: "Berechtigungen",
|
||||||
|
media: "Medien",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "Allgemein", detail: "Details" } },
|
reports: { tabs: { basic: "Allgemein", detail: "Details" } },
|
||||||
@@ -146,6 +149,20 @@ const de: SynapseTranslationMessages = {
|
|||||||
send: "Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
|
send: "Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "Externe Medien",
|
||||||
|
fields: {
|
||||||
|
before_ts: "letzter Zugriff vor",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Externe Medien löschen",
|
||||||
|
send_success: "Die Anfrage zum Löschen externer Medien wurde gesendet.",
|
||||||
|
send_failure: "Bei der Anfrage zum Löschen externer Medien ist ein Fehler aufgetreten.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "Diese API löscht den externen Medien-Cache von der Festplatte Ihres eigenen Servers. Dazu gehören alle lokalen Thumbnails und Kopien heruntergeladener Medien. Diese API beeinflusst nicht die Medien, die in das eigene Medienarchiv des Servers hochgeladen wurden.",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "Benutzer",
|
name: "Benutzer",
|
||||||
@@ -159,11 +176,13 @@ const de: SynapseTranslationMessages = {
|
|||||||
is_guest: "Gast",
|
is_guest: "Gast",
|
||||||
admin: "Server Administrator",
|
admin: "Server Administrator",
|
||||||
locked: "Gesperrt",
|
locked: "Gesperrt",
|
||||||
|
suspended: "Suspendiert",
|
||||||
deactivated: "Deaktiviert",
|
deactivated: "Deaktiviert",
|
||||||
erased: "Gelöscht",
|
erased: "Gelöscht",
|
||||||
guests: "Zeige Gäste",
|
guests: "Zeige Gäste",
|
||||||
show_deactivated: "Zeige deaktivierte Benutzer",
|
show_deactivated: "Zeige deaktivierte Benutzer",
|
||||||
show_locked: "Zeige gesperrte Benutzer",
|
show_locked: "Zeige gesperrte Benutzer",
|
||||||
|
show_suspended: "Zeige suspendierte Benutzer",
|
||||||
user_id: "Suche Benutzer",
|
user_id: "Suche Benutzer",
|
||||||
displayname: "Anzeigename",
|
displayname: "Anzeigename",
|
||||||
password: "Passwort",
|
password: "Passwort",
|
||||||
@@ -181,6 +200,7 @@ 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.",
|
||||||
create_password: "Generiere ein starkes und sicheres Passwort mit dem Button unten.",
|
create_password: "Generiere ein starkes und sicheres Passwort mit dem Button unten.",
|
||||||
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.",
|
||||||
|
suspend: "Ein gesperrter Benutzer kann sich nicht mehr anmelden und wird in den schreibgeschützten Modus versetzt.",
|
||||||
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_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.",
|
||||||
@@ -211,6 +231,11 @@ const de: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "Die Anzahl der Aktionen, die in einer Sekunde durchgeführt werden können.",
|
messages_per_second_text: "Die Anzahl der Aktionen, die in einer Sekunde durchgeführt werden können.",
|
||||||
burst_count: "Burst-Anzahl",
|
burst_count: "Burst-Anzahl",
|
||||||
burst_count_text: "Die Anzahl der Aktionen, die vor der Begrenzung durchgeführt werden können.",
|
burst_count_text: "Die Anzahl der Aktionen, die vor der Begrenzung durchgeführt werden können.",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "Kontodaten",
|
||||||
|
global: "Globale",
|
||||||
|
rooms: "Räume",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -429,6 +454,18 @@ const de: SynapseTranslationMessages = {
|
|||||||
sender: "Absender",
|
sender: "Absender",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "Medien",
|
||||||
|
fields: {
|
||||||
|
media_id: "Medien ID",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
info: "Dies ist eine Liste der Medien, die in den Raum hochgeladen wurden. Es ist nicht möglich, Medien zu löschen, die in externen Medien-Repositories hochgeladen wurden.",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "Raumverzeichnis",
|
name: "Raumverzeichnis",
|
||||||
fields: {
|
fields: {
|
||||||
|
@@ -29,6 +29,7 @@ const en: SynapseTranslationMessages = {
|
|||||||
sso: "SSO",
|
sso: "SSO",
|
||||||
experimental: "Experimental",
|
experimental: "Experimental",
|
||||||
limits: "Rate Limits",
|
limits: "Rate Limits",
|
||||||
|
account_data: "Account Data",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -38,6 +39,7 @@ const en: SynapseTranslationMessages = {
|
|||||||
members: "Members",
|
members: "Members",
|
||||||
detail: "Details",
|
detail: "Details",
|
||||||
permission: "Permissions",
|
permission: "Permissions",
|
||||||
|
media: "Media",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "Basic", detail: "Details" } },
|
reports: { tabs: { basic: "Basic", detail: "Details" } },
|
||||||
@@ -119,6 +121,20 @@ const en: SynapseTranslationMessages = {
|
|||||||
send: "This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
|
send: "This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "Remote Media",
|
||||||
|
fields: {
|
||||||
|
before_ts: "last access before",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Purge remote media",
|
||||||
|
send_success: "Purge remote media request has been sent.",
|
||||||
|
send_failure: "An error has occurred with the purge remote media request.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "This API purges the remote media cache from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to the server's own media repository.",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "User |||| Users",
|
name: "User |||| Users",
|
||||||
@@ -132,11 +148,13 @@ const en: SynapseTranslationMessages = {
|
|||||||
is_guest: "Guest",
|
is_guest: "Guest",
|
||||||
admin: "Server Administrator",
|
admin: "Server Administrator",
|
||||||
locked: "Locked",
|
locked: "Locked",
|
||||||
|
suspended: "Suspended",
|
||||||
deactivated: "Deactivated",
|
deactivated: "Deactivated",
|
||||||
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",
|
show_locked: "Show locked users",
|
||||||
|
show_suspended: "Show suspended users",
|
||||||
user_id: "Search user",
|
user_id: "Search user",
|
||||||
displayname: "Displayname",
|
displayname: "Displayname",
|
||||||
password: "Password",
|
password: "Password",
|
||||||
@@ -154,6 +172,7 @@ const en: SynapseTranslationMessages = {
|
|||||||
password: "Changing password will log user out of all sessions.",
|
password: "Changing password will log user out of all sessions.",
|
||||||
create_password: "Generate a strong and secure password using the button below.",
|
create_password: "Generate a strong and secure password using the button below.",
|
||||||
deactivate: "You must provide a password to re-activate an account.",
|
deactivate: "You must provide a password to re-activate an account.",
|
||||||
|
suspend: "Suspending user means they are put into a read-only mode.",
|
||||||
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_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.",
|
||||||
@@ -184,6 +203,11 @@ const en: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "The number of actions that can be performed in a second.",
|
messages_per_second_text: "The number of actions that can be performed in a second.",
|
||||||
burst_count: "Burst count",
|
burst_count: "Burst count",
|
||||||
burst_count_text: "How many actions that can be performed before being limited.",
|
burst_count_text: "How many actions that can be performed before being limited.",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "Account Data",
|
||||||
|
global: "Global",
|
||||||
|
rooms: "Rooms",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -401,6 +425,18 @@ const en: SynapseTranslationMessages = {
|
|||||||
sender: "Sender",
|
sender: "Sender",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "Media",
|
||||||
|
fields: {
|
||||||
|
media_id: "Media ID",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
info: "This is a list of media that has been uploaded to the room. It is not possible to delete media that has been uploaded to external media repositories.",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}",
|
||||||
|
},
|
||||||
|
},
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "Room directory",
|
name: "Room directory",
|
||||||
fields: {
|
fields: {
|
||||||
|
@@ -24,7 +24,7 @@ const fa: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
|
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
|
||||||
tabs: { sso: "SSO", experimental: "تجربی", limits: "محدودیت ها" },
|
tabs: { sso: "SSO", experimental: "تجربی", limits: "محدودیت ها", account_data: "داده های کاربر" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
tabs: {
|
tabs: {
|
||||||
@@ -32,6 +32,7 @@ const fa: SynapseTranslationMessages = {
|
|||||||
members: "اعضا",
|
members: "اعضا",
|
||||||
detail: "جزئیات",
|
detail: "جزئیات",
|
||||||
permission: "مجوزها",
|
permission: "مجوزها",
|
||||||
|
media: "رسانه ها",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "اصلی", detail: "جزئیات" } },
|
reports: { tabs: { basic: "اصلی", detail: "جزئیات" } },
|
||||||
@@ -113,6 +114,20 @@ const fa: SynapseTranslationMessages = {
|
|||||||
send: "این API رسانه های محلی را از دیسک سرور خود حذف می کند. این شامل هر تصویر کوچک محلی و کپی از رسانه دانلود شده است. این API بر رسانههایی که در مخازن رسانه خارجی آپلود شدهاند تأثیری نخواهد گذاشت.",
|
send: "این API رسانه های محلی را از دیسک سرور خود حذف می کند. این شامل هر تصویر کوچک محلی و کپی از رسانه دانلود شده است. این API بر رسانههایی که در مخازن رسانه خارجی آپلود شدهاند تأثیری نخواهد گذاشت.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "رسانههای از راه دور",
|
||||||
|
fields: {
|
||||||
|
before_ts: "آخرین دسترسی قبل از",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "پاک کردن رسانههای از راه دور",
|
||||||
|
send_success: "درخواست پاک کردن رسانههای از راه دور ارسال شد.",
|
||||||
|
send_failure: "درخواست برای پاک کردن رسانههای از راه دور با خطا مواجه شد.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "این API کش رسانههای از راه دور را از دیسک سرور شما پاک میکند. این شامل هر گونه بندانگشتی محلی و نسخههای رسانههای دانلود شده میشود. این API بر رسانههای آپلود شده به مخزن رسانه سرور تأثیری نخواهد داشت.",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "کاربر |||| کاربران",
|
name: "کاربر |||| کاربران",
|
||||||
@@ -126,9 +141,12 @@ const fa: SynapseTranslationMessages = {
|
|||||||
is_guest: "مهمان",
|
is_guest: "مهمان",
|
||||||
admin: "مدیر سرور",
|
admin: "مدیر سرور",
|
||||||
deactivated: "غیرفعال",
|
deactivated: "غیرفعال",
|
||||||
|
locked: "قفل شده",
|
||||||
|
suspended: "معلق",
|
||||||
guests: "نمایش مهمانان",
|
guests: "نمایش مهمانان",
|
||||||
show_deactivated: "نمایش کاربران غیرفعال شده",
|
show_deactivated: "نمایش کاربران غیرفعال شده",
|
||||||
show_locked: "نمایش کاربران قفل شده",
|
show_locked: "نمایش کاربران قفل شده",
|
||||||
|
show_suspended: "نمایش کاربران معلق",
|
||||||
user_id: "جستجوی کاربر",
|
user_id: "جستجوی کاربر",
|
||||||
displayname: "نام نمایشی",
|
displayname: "نام نمایشی",
|
||||||
password: "رمز عبور",
|
password: "رمز عبور",
|
||||||
@@ -146,6 +164,7 @@ const fa: SynapseTranslationMessages = {
|
|||||||
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
|
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
|
||||||
create_password: "رمز عبور قوی و امنی را با استفاده از دکمه زیر ایجاد کنید.",
|
create_password: "رمز عبور قوی و امنی را با استفاده از دکمه زیر ایجاد کنید.",
|
||||||
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
||||||
|
suspend: "کاربران معلق نمی توانند وارد شوند و پیام های آنها به دیگران نمایش داده نمی شود.",
|
||||||
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
|
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
|
||||||
erase_text: "وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.",
|
erase_text: "وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.",
|
||||||
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
|
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
|
||||||
@@ -176,6 +195,11 @@ const fa: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "تعداد عملیاتی که می تواند در یک ثانیه انجام شود.",
|
messages_per_second_text: "تعداد عملیاتی که می تواند در یک ثانیه انجام شود.",
|
||||||
burst_count: "تعداد پیچیدگی",
|
burst_count: "تعداد پیچیدگی",
|
||||||
burst_count_text: "تعداد عملیاتی که می تواند قبل از محدودیت انجام شود.",
|
burst_count_text: "تعداد عملیاتی که می تواند قبل از محدودیت انجام شود.",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "داده های کاربر",
|
||||||
|
global: "عمومی",
|
||||||
|
rooms: "اتاق ها",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -380,6 +404,18 @@ const fa: SynapseTranslationMessages = {
|
|||||||
sender: "فرستنده",
|
sender: "فرستنده",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "رسانه ها",
|
||||||
|
fields: {
|
||||||
|
media_id: "شناسه رسانه",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
info: "این یک لیست از رسانه ها است که در اتاق بارگذاری شده است. نمی توان رسانه ها را حذف کرد که در اتاق های خارجی بارگذاری شده اند.",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "راهنمای اتاق",
|
name: "راهنمای اتاق",
|
||||||
fields: {
|
fields: {
|
||||||
|
@@ -24,7 +24,7 @@ const fr: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
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.",
|
||||||
tabs: { sso: "Authentification unique", experimental: "Expérimental", limits: "Limites" },
|
tabs: { sso: "Authentification unique", experimental: "Expérimental", limits: "Limites", account_data: "Données du compte" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
tabs: {
|
tabs: {
|
||||||
@@ -32,6 +32,7 @@ const fr: SynapseTranslationMessages = {
|
|||||||
members: "Membres",
|
members: "Membres",
|
||||||
detail: "Détails",
|
detail: "Détails",
|
||||||
permission: "Permissions",
|
permission: "Permissions",
|
||||||
|
media: "Médias",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "Informations de base", detail: "Détails" } },
|
reports: { tabs: { basic: "Informations de base", detail: "Détails" } },
|
||||||
@@ -116,6 +117,20 @@ const fr: SynapseTranslationMessages = {
|
|||||||
send: "Cette API supprime les médias locaux du disque de votre propre serveur. Cela inclut toutes les vignettes locales et les copies des médias téléchargés. Cette API n'affectera pas les médias qui ont été téléversés dans des dépôts de médias externes.",
|
send: "Cette API supprime les médias locaux du disque de votre propre serveur. Cela inclut toutes les vignettes locales et les copies des médias téléchargés. Cette API n'affectera pas les médias qui ont été téléversés dans des dépôts de médias externes.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "Médias distants",
|
||||||
|
fields: {
|
||||||
|
before_ts: "dernier accès avant",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Purger les médias distants",
|
||||||
|
send_success: "La demande de purge des médias distants a été envoyée.",
|
||||||
|
send_failure: "Une erreur est survenue lors de la demande de purge des médias distants.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "Cette API purge le cache des médias distants du disque de votre propre serveur. Cela inclut toutes les vignettes locales et les copies des médias téléchargés. Cette API n'affectera pas les médias qui ont été téléchargés dans le dépôt de médias du serveur.",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "Utilisateur |||| Utilisateurs",
|
name: "Utilisateur |||| Utilisateurs",
|
||||||
@@ -129,10 +144,12 @@ const fr: SynapseTranslationMessages = {
|
|||||||
is_guest: "Visiteur",
|
is_guest: "Visiteur",
|
||||||
admin: "Administrateur du serveur",
|
admin: "Administrateur du serveur",
|
||||||
locked: "Verrouillé",
|
locked: "Verrouillé",
|
||||||
|
suspended: "Suspendu",
|
||||||
deactivated: "Désactivé",
|
deactivated: "Désactivé",
|
||||||
guests: "Afficher les visiteurs",
|
guests: "Afficher les visiteurs",
|
||||||
show_deactivated: "Afficher les utilisateurs désactivés",
|
show_deactivated: "Afficher les utilisateurs désactivés",
|
||||||
show_locked: "Afficher les utilisateurs verrouillés",
|
show_locked: "Afficher les utilisateurs verrouillés",
|
||||||
|
show_suspended: "Afficher les utilisateurs suspendus",
|
||||||
user_id: "Rechercher un utilisateur",
|
user_id: "Rechercher un utilisateur",
|
||||||
displayname: "Nom d'affichage",
|
displayname: "Nom d'affichage",
|
||||||
password: "Mot de passe",
|
password: "Mot de passe",
|
||||||
@@ -149,6 +166,7 @@ const fr: SynapseTranslationMessages = {
|
|||||||
password: "Changer le mot de passe déconnectera l'utilisateur de toutes les sessions.",
|
password: "Changer le mot de passe déconnectera l'utilisateur de toutes les sessions.",
|
||||||
create_password: "Générer un mot de passe fort et sécurisé en utilisant le bouton ci-dessous.",
|
create_password: "Générer un mot de passe fort et sécurisé en utilisant le bouton ci-dessous.",
|
||||||
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.",
|
||||||
|
suspend: "L'utilisateur sera suspendu jusqu'à ce que vous le réactiviez.",
|
||||||
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_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.",
|
||||||
@@ -179,6 +197,11 @@ const fr: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "Le nombre d'actions que l'utilisateur peut effectuer par seconde.",
|
messages_per_second_text: "Le nombre d'actions que l'utilisateur peut effectuer par seconde.",
|
||||||
burst_count: "Compteur de pics",
|
burst_count: "Compteur de pics",
|
||||||
burst_count_text: "Le nombre d'actions que l'utilisateur peut effectuer avant d'être limité.",
|
burst_count_text: "Le nombre d'actions que l'utilisateur peut effectuer avant d'être limité.",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "Données du compte",
|
||||||
|
global: "Globales",
|
||||||
|
rooms: "Salons",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -383,6 +406,18 @@ const fr: SynapseTranslationMessages = {
|
|||||||
sender: "Expéditeur",
|
sender: "Expéditeur",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "Médias",
|
||||||
|
fields: {
|
||||||
|
media_id: "Identifiant du média",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
info: "Cette liste contient les médias qui ont été téléchargés dans le salon. Il n'est pas possible de supprimer les médias qui ont été téléversés dans des dépôts de médias externes.",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "Répertoire des salons",
|
name: "Répertoire des salons",
|
||||||
fields: {
|
fields: {
|
||||||
|
37
src/i18n/index.d.ts
vendored
37
src/i18n/index.d.ts
vendored
@@ -22,7 +22,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
};
|
};
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: string;
|
invalid_user_id: string;
|
||||||
tabs: { sso: string; experimental: string; limits: string; };
|
tabs: { sso: string; experimental: string; limits: string; account_data: string; };
|
||||||
};
|
};
|
||||||
rooms: {
|
rooms: {
|
||||||
details?: string; // TODO: fa, fr, it, zh
|
details?: string; // TODO: fa, fr, it, zh
|
||||||
@@ -31,6 +31,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
members: string;
|
members: string;
|
||||||
detail: string;
|
detail: string;
|
||||||
permission: string;
|
permission: string;
|
||||||
|
media: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
reports: { tabs: { basic: string; detail: string } };
|
reports: { tabs: { basic: string; detail: string } };
|
||||||
@@ -111,6 +112,20 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
send: string;
|
send: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
purge_remote_media: {
|
||||||
|
name: string;
|
||||||
|
fields: {
|
||||||
|
before_ts: string;
|
||||||
|
};
|
||||||
|
action: {
|
||||||
|
send: string;
|
||||||
|
send_success: string;
|
||||||
|
send_failure: string;
|
||||||
|
};
|
||||||
|
helper: {
|
||||||
|
send: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -124,11 +139,13 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
is_guest: string;
|
is_guest: string;
|
||||||
admin: string;
|
admin: string;
|
||||||
locked?: string; // TODO: fa, zh
|
locked?: string; // TODO: fa, zh
|
||||||
|
suspended: string;
|
||||||
deactivated: string;
|
deactivated: string;
|
||||||
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
|
show_locked?: string; // TODO: de, fa, fr, it, zh
|
||||||
|
show_suspended: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
displayname: string;
|
displayname: string;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -146,6 +163,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
password: string;
|
password: string;
|
||||||
create_password: string;
|
create_password: string;
|
||||||
deactivate: string;
|
deactivate: string;
|
||||||
|
suspend: string;
|
||||||
erase: string;
|
erase: string;
|
||||||
erase_text: string;
|
erase_text: string;
|
||||||
erase_admin_error: string;
|
erase_admin_error: string;
|
||||||
@@ -177,6 +195,11 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
burst_count: string;
|
burst_count: string;
|
||||||
burst_count_text: string;
|
burst_count_text: string;
|
||||||
};
|
};
|
||||||
|
account_data: {
|
||||||
|
title: string;
|
||||||
|
global: string;
|
||||||
|
rooms: string;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
rooms: {
|
rooms: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -393,6 +416,18 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
sender: string;
|
sender: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
room_media?: {
|
||||||
|
name: string;
|
||||||
|
fields: {
|
||||||
|
media_id: string;
|
||||||
|
};
|
||||||
|
helper: {
|
||||||
|
info: string;
|
||||||
|
};
|
||||||
|
action: {
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
room_directory?: {
|
room_directory?: {
|
||||||
name: string;
|
name: string;
|
||||||
fields: {
|
fields: {
|
||||||
|
@@ -24,7 +24,7 @@ const it: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "ID utente non valido su questo homeserver.",
|
invalid_user_id: "ID utente non valido su questo homeserver.",
|
||||||
tabs: { sso: "SSO", experimental: "Sperimentale", limits: "Limiti" },
|
tabs: { sso: "SSO", experimental: "Sperimentale", limits: "Limiti", account_data: "Dati del profilo" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
tabs: {
|
tabs: {
|
||||||
@@ -32,6 +32,7 @@ const it: SynapseTranslationMessages = {
|
|||||||
members: "Membro",
|
members: "Membro",
|
||||||
detail: "Dettagli",
|
detail: "Dettagli",
|
||||||
permission: "Permessi",
|
permission: "Permessi",
|
||||||
|
media: "Media",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "Semplice", detail: "Dettagli" } },
|
reports: { tabs: { basic: "Semplice", detail: "Dettagli" } },
|
||||||
@@ -113,6 +114,20 @@ const it: SynapseTranslationMessages = {
|
|||||||
send: "Questa API cancella i media locali dal disco del tuo server. Questo include anche ogni miniatura e copia del media scaricato. Questa API non inciderà sui media che sono stati caricati nei repository esterni.",
|
send: "Questa API cancella i media locali dal disco del tuo server. Questo include anche ogni miniatura e copia del media scaricato. Questa API non inciderà sui media che sono stati caricati nei repository esterni.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "Media Remoti",
|
||||||
|
fields: {
|
||||||
|
before_ts: "ultimo accesso prima di",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Elimina media remoti",
|
||||||
|
send_success: "La richiesta per eliminare i media remoti è stata inviata.",
|
||||||
|
send_failure: "Si è verificato un errore con la richiesta di eliminazione dei media remoti.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "Questa API elimina la cache dei media remoti dal disco del tuo server. Questo include qualsiasi miniatura locale e copie di media scaricati. Questa API non influirà sui media che sono stati caricati nel repository multimediale del server.",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "Utente |||| Utenti",
|
name: "Utente |||| Utenti",
|
||||||
@@ -126,10 +141,12 @@ const it: SynapseTranslationMessages = {
|
|||||||
is_guest: "Ospite",
|
is_guest: "Ospite",
|
||||||
admin: "Amministratore",
|
admin: "Amministratore",
|
||||||
locked: "Bloccato",
|
locked: "Bloccato",
|
||||||
|
suspended: "Sospeso",
|
||||||
deactivated: "Disattivato",
|
deactivated: "Disattivato",
|
||||||
guests: "Mostra gli ospiti",
|
guests: "Mostra gli ospiti",
|
||||||
show_deactivated: "Mostra gli utenti disattivati",
|
show_deactivated: "Mostra gli utenti disattivati",
|
||||||
show_locked: "Mostra gli utenti bloccati",
|
show_locked: "Mostra gli utenti bloccati",
|
||||||
|
show_suspended: "Mostra gli utenti sospesi",
|
||||||
user_id: "Cerca utente",
|
user_id: "Cerca utente",
|
||||||
displayname: "Nickname",
|
displayname: "Nickname",
|
||||||
password: "Password",
|
password: "Password",
|
||||||
@@ -147,6 +164,7 @@ 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.",
|
||||||
create_password: "Genera una password forte e sicura utilizzando il pulsante sottostante.",
|
create_password: "Genera una password forte e sicura utilizzando il pulsante sottostante.",
|
||||||
deactivate: "Devi fornire una password per riattivare l'account.",
|
deactivate: "Devi fornire una password per riattivare l'account.",
|
||||||
|
suspend: "Sospendi l'utente",
|
||||||
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_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.",
|
erase_admin_error: "Non è consentito eliminare il proprio utente.",
|
||||||
@@ -177,6 +195,11 @@ const it: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "Il numero di azioni che l'utente può eseguire al secondo.",
|
messages_per_second_text: "Il numero di azioni che l'utente può eseguire al secondo.",
|
||||||
burst_count: "Burst-conteggio",
|
burst_count: "Burst-conteggio",
|
||||||
burst_count_text: "Il numero di azioni che l'utente può eseguire prima di essere limitato.",
|
burst_count_text: "Il numero di azioni che l'utente può eseguire prima di essere limitato.",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "Dati del profilo",
|
||||||
|
global: "Globale",
|
||||||
|
rooms: "Stanza",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -376,6 +399,18 @@ const it: SynapseTranslationMessages = {
|
|||||||
sender: "Mittente",
|
sender: "Mittente",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "Media",
|
||||||
|
fields: {
|
||||||
|
media_id: "ID Media",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
info: "Questo è un elenco dei media caricati nella stanza. Non è possibile eliminare i media caricati su repository esterni.",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "Elenco delle stanze",
|
name: "Elenco delle stanze",
|
||||||
fields: {
|
fields: {
|
||||||
|
@@ -11,6 +11,10 @@ const fixedRussianMessages = {
|
|||||||
no_filtered_results: "Нет результатов",
|
no_filtered_results: "Нет результатов",
|
||||||
clear_filters: "Все фильтры сбросить",
|
clear_filters: "Все фильтры сбросить",
|
||||||
},
|
},
|
||||||
|
action: {
|
||||||
|
...russianMessages.ra.action,
|
||||||
|
select_all_button: "Выбрать все",
|
||||||
|
},
|
||||||
page: {
|
page: {
|
||||||
...russianMessages.ra.page,
|
...russianMessages.ra.page,
|
||||||
empty: "Пусто",
|
empty: "Пусто",
|
||||||
@@ -23,6 +27,7 @@ const fixedRussianMessages = {
|
|||||||
"У вас нет прав доступа к этой странице.",
|
"У вас нет прав доступа к этой странице.",
|
||||||
authentication_error:
|
authentication_error:
|
||||||
"Сервер аутентификации вернул ошибку и не смог проверить ваши учетные данные.",
|
"Сервер аутентификации вернул ошибку и не смог проверить ваши учетные данные.",
|
||||||
|
select_all_limit_reached: "Слишком много элементов для выбора. Были выбраны только первые %{max} элементов.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -50,7 +55,7 @@ const ru: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
|
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
|
||||||
tabs: { sso: "SSO", experimental: "Экспериментальные", limits: "Ограничения" },
|
tabs: { sso: "SSO", experimental: "Экспериментальные", limits: "Ограничения", account_data: "Данные пользователя" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
details: "Данные комнаты",
|
details: "Данные комнаты",
|
||||||
@@ -59,6 +64,7 @@ const ru: SynapseTranslationMessages = {
|
|||||||
members: "Участники",
|
members: "Участники",
|
||||||
detail: "Подробности",
|
detail: "Подробности",
|
||||||
permission: "Права доступа",
|
permission: "Права доступа",
|
||||||
|
media: "Медиа",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "Основные", detail: "Подробности" } },
|
reports: { tabs: { basic: "Основные", detail: "Подробности" } },
|
||||||
@@ -149,6 +155,20 @@ const ru: SynapseTranslationMessages = {
|
|||||||
Данный API не затрагивает файлы, загруженные во внешние хранилища.",
|
Данный API не затрагивает файлы, загруженные во внешние хранилища.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "Внешние медиа",
|
||||||
|
fields: {
|
||||||
|
before_ts: "последний доступ до",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Очистить внешние медиа",
|
||||||
|
send_success: "Запрос на очистку внешних медиа был отправлен.",
|
||||||
|
send_failure: "Произошла ошибка при запросе очистки внешних медиа.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "Этот API очищает кэш внешних медиа с диска вашего сервера. Это включает любые локальные миниатюры и копии загруженных медиа. Этот API не повлияет на медиа, которые были загружены в собственное медиа-хранилище сервера.",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "Пользователь |||| Пользователи",
|
name: "Пользователь |||| Пользователи",
|
||||||
@@ -162,11 +182,13 @@ const ru: SynapseTranslationMessages = {
|
|||||||
is_guest: "Гость",
|
is_guest: "Гость",
|
||||||
admin: "Администратор сервера",
|
admin: "Администратор сервера",
|
||||||
locked: "Заблокирован",
|
locked: "Заблокирован",
|
||||||
|
suspended: "Приостановлен",
|
||||||
deactivated: "Деактивирован",
|
deactivated: "Деактивирован",
|
||||||
erased: "Удалён",
|
erased: "Удалён",
|
||||||
guests: "Показывать гостей",
|
guests: "Показывать гостей",
|
||||||
show_deactivated: "Показывать деактивированных",
|
show_deactivated: "Показывать деактивированных",
|
||||||
show_locked: "Показывать заблокированных",
|
show_locked: "Показывать заблокированных",
|
||||||
|
show_suspended: "Показывать приостановленных",
|
||||||
user_id: "Поиск пользователя",
|
user_id: "Поиск пользователя",
|
||||||
displayname: "Отображаемое имя",
|
displayname: "Отображаемое имя",
|
||||||
password: "Пароль",
|
password: "Пароль",
|
||||||
@@ -184,6 +206,7 @@ const ru: SynapseTranslationMessages = {
|
|||||||
password: "Смена пароля завершит все сессии пользователя.",
|
password: "Смена пароля завершит все сессии пользователя.",
|
||||||
create_password: "Сгенерировать надёжный и безопасный пароль, используя кнопку ниже.",
|
create_password: "Сгенерировать надёжный и безопасный пароль, используя кнопку ниже.",
|
||||||
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
||||||
|
suspend: "Приостановка учётной записи означает, что пользователь не сможет войти в свою учётную запись, пока она не будет снова активирована.",
|
||||||
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
||||||
erase_text: "Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
|
erase_text: "Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
|
||||||
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
||||||
@@ -214,6 +237,11 @@ const ru: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "Количество действий, которые могут быть выполнены в секунду.",
|
messages_per_second_text: "Количество действий, которые могут быть выполнены в секунду.",
|
||||||
burst_count: "Burst-счётчик",
|
burst_count: "Burst-счётчик",
|
||||||
burst_count_text: "Количество действий, которые могут быть выполнены до ограничения.",
|
burst_count_text: "Количество действий, которые могут быть выполнены до ограничения.",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "Данные пользователя",
|
||||||
|
global: "Глобальные",
|
||||||
|
rooms: "Комнаты",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -434,6 +462,18 @@ const ru: SynapseTranslationMessages = {
|
|||||||
sender: "Отправитель",
|
sender: "Отправитель",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "Медиа",
|
||||||
|
fields: {
|
||||||
|
media_id: "ID медиа",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
info: "Это список медиа, которые были загружены в комнату. Невозможно удалить медиа, которые были загружены в внешние медиа-репозитории.",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "Каталог комнат",
|
name: "Каталог комнат",
|
||||||
fields: {
|
fields: {
|
||||||
|
@@ -13,7 +13,8 @@ const fixedChineseMessages = {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
...chineseMessages.ra.action,
|
...chineseMessages.ra.action,
|
||||||
update_application: "Anwendung aktualisieren",
|
update_application: "更新应用",
|
||||||
|
select_all_button: "全部选择",
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
...chineseMessages.ra.page,
|
...chineseMessages.ra.page,
|
||||||
@@ -26,6 +27,7 @@ const fixedChineseMessages = {
|
|||||||
"您没有访问此页面的权限。",
|
"您没有访问此页面的权限。",
|
||||||
authentication_error:
|
authentication_error:
|
||||||
"身份验证服务器返回错误,无法验证您的凭据。",
|
"身份验证服务器返回错误,无法验证您的凭据。",
|
||||||
|
select_all_limit_reached: "选择的元素太多。只选择了前 %{max} 个元素。",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -52,7 +54,7 @@ const zh: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
||||||
tabs: { sso: "SSO", experimental: "实验性", limits: "限制" },
|
tabs: { sso: "SSO", experimental: "实验性", limits: "限制", account_data: "账户数据" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
tabs: {
|
tabs: {
|
||||||
@@ -60,6 +62,7 @@ const zh: SynapseTranslationMessages = {
|
|||||||
members: "成员",
|
members: "成员",
|
||||||
detail: "细节",
|
detail: "细节",
|
||||||
permission: "权限",
|
permission: "权限",
|
||||||
|
media: "媒体",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "基本", detail: "细节" } },
|
reports: { tabs: { basic: "基本", detail: "细节" } },
|
||||||
@@ -141,6 +144,20 @@ const zh: SynapseTranslationMessages = {
|
|||||||
send: "这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
|
send: "这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "远程媒体",
|
||||||
|
fields: {
|
||||||
|
before_ts: "最后访问于之前",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "清除远程媒体",
|
||||||
|
send_success: "远程媒体清除请求已发送。",
|
||||||
|
send_failure: "发生错误,远程媒体清除请求未成功。",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "此API清除您服务器磁盘上的远程媒体缓存。这包括任何本地缩略图和下载的媒体副本。此API不会影响已经上传到服务器媒体存储库的媒体。",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "用户",
|
name: "用户",
|
||||||
@@ -153,10 +170,13 @@ const zh: SynapseTranslationMessages = {
|
|||||||
name: "用户名",
|
name: "用户名",
|
||||||
is_guest: "访客",
|
is_guest: "访客",
|
||||||
admin: "服务器管理员",
|
admin: "服务器管理员",
|
||||||
|
locked: "锁定",
|
||||||
deactivated: "被禁用",
|
deactivated: "被禁用",
|
||||||
|
suspended: "被锁定",
|
||||||
guests: "显示访客",
|
guests: "显示访客",
|
||||||
show_deactivated: "显示被禁用的账户",
|
show_deactivated: "显示被禁用的账户",
|
||||||
show_locked: "显示被锁定的账户",
|
show_locked: "显示被锁定的账户",
|
||||||
|
show_suspended: "显示被锁定的账户",
|
||||||
user_id: "搜索用户",
|
user_id: "搜索用户",
|
||||||
displayname: "显示名字",
|
displayname: "显示名字",
|
||||||
password: "密码",
|
password: "密码",
|
||||||
@@ -172,6 +192,7 @@ const zh: SynapseTranslationMessages = {
|
|||||||
password: "更改密码会使用户注销所有会话。",
|
password: "更改密码会使用户注销所有会话。",
|
||||||
create_password: "使用下面的按钮生成一个强大和安全的密码。",
|
create_password: "使用下面的按钮生成一个强大和安全的密码。",
|
||||||
deactivate: "您必须提供一串密码来激活账户。",
|
deactivate: "您必须提供一串密码来激活账户。",
|
||||||
|
suspend: "您必须提供一串密码来暂停账户。",
|
||||||
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
||||||
erase_text: "这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
|
erase_text: "这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
|
||||||
erase_admin_error: "不允许删除自己的用户",
|
erase_admin_error: "不允许删除自己的用户",
|
||||||
@@ -202,6 +223,11 @@ const zh: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "每秒可以执行的操作数。",
|
messages_per_second_text: "每秒可以执行的操作数。",
|
||||||
burst_count: "Burst-计数",
|
burst_count: "Burst-计数",
|
||||||
burst_count_text: "在限制之前可以执行的操作数。",
|
burst_count_text: "在限制之前可以执行的操作数。",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "账户数据",
|
||||||
|
global: "全局",
|
||||||
|
rooms: "房间",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -344,6 +370,18 @@ const zh: SynapseTranslationMessages = {
|
|||||||
media_length: "媒体文件长度",
|
media_length: "媒体文件长度",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "媒体",
|
||||||
|
fields: {
|
||||||
|
media_id: "媒体ID",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
info: "这是上传到房间的媒体列表。无法删除上传到外部媒体存储库的媒体。",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export default zh;
|
export default zh;
|
||||||
|
@@ -2,7 +2,8 @@ import React from "react";
|
|||||||
|
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
import {App, AppContext } from "./App";
|
import { App } from "./App";
|
||||||
|
import { AppContext } from "./Context";
|
||||||
import { FetchConfig, GetConfig } from "./utils/config";
|
import { FetchConfig, GetConfig } from "./utils/config";
|
||||||
|
|
||||||
await FetchConfig();
|
await FetchConfig();
|
||||||
|
@@ -2,9 +2,10 @@ import polyglotI18nProvider from "ra-i18n-polyglot";
|
|||||||
|
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { AdminContext } from "react-admin";
|
import { AdminContext } from "react-admin";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
import LoginPage from "./LoginPage";
|
import LoginPage from "./LoginPage";
|
||||||
import { AppContext } from "../App";
|
import { AppContext } from "../Context";
|
||||||
import englishMessages from "../i18n/en";
|
import englishMessages from "../i18n/en";
|
||||||
|
|
||||||
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
|
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
|
||||||
@@ -14,9 +15,11 @@ describe("LoginForm", () => {
|
|||||||
it("renders with no restriction to homeserver", async () => {
|
it("renders with no restriction to homeserver", async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
<AdminContext i18nProvider={i18nProvider}>
|
<AdminContext i18nProvider={i18nProvider}>
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
</AdminContext>
|
</AdminContext>
|
||||||
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,11 +36,15 @@ describe("LoginForm", () => {
|
|||||||
|
|
||||||
it("renders with single restricted homeserver", () => {
|
it("renders with single restricted homeserver", () => {
|
||||||
render(
|
render(
|
||||||
<AppContext.Provider value={{ restrictBaseUrl: "https://matrix.example.com" }}>
|
<BrowserRouter>
|
||||||
|
<AppContext.Provider
|
||||||
|
value={{ restrictBaseUrl: "https://matrix.example.com", asManagedUsers: [], menu: [] }}
|
||||||
|
>
|
||||||
<AdminContext i18nProvider={i18nProvider}>
|
<AdminContext i18nProvider={i18nProvider}>
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
</AdminContext>
|
</AdminContext>
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
screen.getByText(englishMessages.synapseadmin.auth.welcome);
|
screen.getByText(englishMessages.synapseadmin.auth.welcome);
|
||||||
@@ -56,10 +63,14 @@ describe("LoginForm", () => {
|
|||||||
<AppContext.Provider
|
<AppContext.Provider
|
||||||
value={{
|
value={{
|
||||||
restrictBaseUrl: ["https://matrix.example.com", "https://matrix.example.org"],
|
restrictBaseUrl: ["https://matrix.example.com", "https://matrix.example.org"],
|
||||||
|
asManagedUsers: [],
|
||||||
|
menu: [],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AdminContext i18nProvider={i18nProvider}>
|
<AdminContext i18nProvider={i18nProvider}>
|
||||||
|
<BrowserRouter>
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
|
</BrowserRouter>
|
||||||
</AdminContext>
|
</AdminContext>
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
);
|
);
|
||||||
|
@@ -16,7 +16,7 @@ import {
|
|||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import LoginFormBox from "../components/LoginFormBox";
|
import LoginFormBox from "../components/LoginFormBox";
|
||||||
import { useAppContext } from "../App";
|
import { useAppContext } from "../Context";
|
||||||
import {
|
import {
|
||||||
getServerVersion,
|
getServerVersion,
|
||||||
getSupportedFeatures,
|
getSupportedFeatures,
|
||||||
@@ -47,7 +47,7 @@ const LoginPage = () => {
|
|||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url");
|
const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url");
|
||||||
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
||||||
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
const loginToken = new URLSearchParams(window.location.search).get("loginToken")
|
||||||
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
|
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,10 +55,11 @@ const LoginPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ssoToken = loginToken[1];
|
console.log("SSO token is", loginToken);
|
||||||
console.log("SSO token is", ssoToken);
|
|
||||||
// Prevent further requests
|
// Prevent further requests
|
||||||
window.history.replaceState({}, "", window.location.href.replace(loginToken[0], "#").split("#")[0]);
|
const previousUrl = new URL(window.location.toString())
|
||||||
|
previousUrl.searchParams.delete("loginToken")
|
||||||
|
window.history.replaceState({}, "", previousUrl.toString());
|
||||||
const baseUrl = localStorage.getItem("sso_base_url");
|
const baseUrl = localStorage.getItem("sso_base_url");
|
||||||
localStorage.removeItem("sso_base_url");
|
localStorage.removeItem("sso_base_url");
|
||||||
if (baseUrl) {
|
if (baseUrl) {
|
||||||
@@ -66,10 +67,10 @@ const LoginPage = () => {
|
|||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
username: null,
|
username: null,
|
||||||
password: null,
|
password: null,
|
||||||
loginToken: ssoToken,
|
loginToken,
|
||||||
};
|
};
|
||||||
console.log("Base URL is:", baseUrl);
|
console.log("Base URL is:", baseUrl);
|
||||||
console.log("SSO Token is:", ssoToken);
|
console.log("SSO Token is:", loginToken);
|
||||||
console.log("Let's try token login...");
|
console.log("Let's try token login...");
|
||||||
login(auth).catch(error => {
|
login(auth).catch(error => {
|
||||||
alert(
|
alert(
|
||||||
|
@@ -7,12 +7,12 @@ import PageviewIcon from "@mui/icons-material/Pageview";
|
|||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
import RoomIcon from "@mui/icons-material/ViewList";
|
import RoomIcon from "@mui/icons-material/ViewList";
|
||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
|
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateField,
|
DateField,
|
||||||
EditButton,
|
|
||||||
WrapperField,
|
WrapperField,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DatagridConfigurable,
|
DatagridConfigurable,
|
||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
useTranslate,
|
useTranslate,
|
||||||
useListContext,
|
useListContext,
|
||||||
useNotify,
|
useNotify,
|
||||||
|
DeleteButton,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
@@ -49,6 +50,7 @@ import {
|
|||||||
} from "./room_directory";
|
} from "./room_directory";
|
||||||
import { DATE_FORMAT } from "../utils/date";
|
import { DATE_FORMAT } from "../utils/date";
|
||||||
import DeleteRoomButton from "../components/DeleteRoomButton";
|
import DeleteRoomButton from "../components/DeleteRoomButton";
|
||||||
|
import { MediaIDField } from "../components/media";
|
||||||
import AvatarField from "../components/AvatarField";
|
import AvatarField from "../components/AvatarField";
|
||||||
import { Room } from "../synapse/dataProvider";
|
import { Room } from "../synapse/dataProvider";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
@@ -58,6 +60,8 @@ import { useState } from "react";
|
|||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import PersonIcon from '@mui/icons-material/Person';
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
|
||||||
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||||
|
|
||||||
const RoomTitle = () => {
|
const RoomTitle = () => {
|
||||||
@@ -231,6 +235,16 @@ export const RoomShow = (props: ShowProps) => {
|
|||||||
</ReferenceManyField>
|
</ReferenceManyField>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab label="synapseadmin.rooms.tabs.media" icon={<PermMediaIcon />} path="media">
|
||||||
|
<Alert severity="warning">{translate("resources.room_media.helper.info")}</Alert>
|
||||||
|
<ReferenceManyField reference="room_media" target="room_id" label={false}>
|
||||||
|
<Datagrid sx={{ width: "100%" }} bulkActionButtons={false}>
|
||||||
|
<MediaIDField source="media_id" />
|
||||||
|
<DeleteButton mutationMode="pessimistic" redirect={false} />
|
||||||
|
</Datagrid>
|
||||||
|
</ReferenceManyField>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
<Tab label="synapseadmin.rooms.tabs.permission" icon={<VisibilityIcon />} path="permission">
|
<Tab label="synapseadmin.rooms.tabs.permission" icon={<VisibilityIcon />} path="permission">
|
||||||
<BooleanField source="federatable" />
|
<BooleanField source="federatable" />
|
||||||
<BooleanField source="public" />
|
<BooleanField source="public" />
|
||||||
|
@@ -14,13 +14,14 @@ import {
|
|||||||
useListContext,
|
useListContext,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
|
||||||
import { DeleteMediaButton } from "../components/media";
|
import { DeleteMediaButton, PurgeRemoteMediaButton } from "../components/media";
|
||||||
|
|
||||||
const ListActions = () => {
|
const ListActions = () => {
|
||||||
const { isLoading, total } = useListContext();
|
const { isLoading, total } = useListContext();
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
<DeleteMediaButton />
|
<DeleteMediaButton />
|
||||||
|
<PurgeRemoteMediaButton />
|
||||||
<ExportButton disabled={isLoading || total === 0} />
|
<ExportButton disabled={isLoading || total === 0} />
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
|
@@ -10,6 +10,7 @@ import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputCompone
|
|||||||
import ScienceIcon from "@mui/icons-material/Science";
|
import ScienceIcon from "@mui/icons-material/Science";
|
||||||
import LockClockIcon from '@mui/icons-material/LockClock';
|
import LockClockIcon from '@mui/icons-material/LockClock';
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
|
import DocumentScannerIcon from "@mui/icons-material/DocumentScanner";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Alert, Typography } from "@mui/material";
|
import { Alert, Typography } from "@mui/material";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
@@ -50,6 +51,7 @@ import {
|
|||||||
SaveButton,
|
SaveButton,
|
||||||
CreateButton,
|
CreateButton,
|
||||||
ExportButton,
|
ExportButton,
|
||||||
|
BulkDeleteButton,
|
||||||
TopToolbar,
|
TopToolbar,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
NumberField,
|
NumberField,
|
||||||
@@ -80,6 +82,8 @@ import ExperimentalFeaturesList from "../components/ExperimentalFeatures";
|
|||||||
import UserRateLimits from "../components/UserRateLimits";
|
import UserRateLimits from "../components/UserRateLimits";
|
||||||
import { User, UsernameAvailabilityResult } from "../synapse/dataProvider";
|
import { User, UsernameAvailabilityResult } from "../synapse/dataProvider";
|
||||||
import { MakeAdminBtn } from "./rooms";
|
import { MakeAdminBtn } from "./rooms";
|
||||||
|
import UserAccountData from "../components/UserAccountData";
|
||||||
|
import decodeURLComponent from "../utils/decodeURLComponent";
|
||||||
|
|
||||||
const choices_medium = [
|
const choices_medium = [
|
||||||
{ id: "email", name: "resources.users.email" },
|
{ id: "email", name: "resources.users.email" },
|
||||||
@@ -111,6 +115,7 @@ const userFilters = [
|
|||||||
<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 />,
|
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
||||||
|
<BooleanInput label="resources.users.fields.show_suspended" source="suspended" alwaysOn />,
|
||||||
];
|
];
|
||||||
|
|
||||||
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSelected: boolean; asManagedUserIsSelected: boolean }> = props => {
|
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSelected: boolean; asManagedUserIsSelected: boolean }> = props => {
|
||||||
@@ -162,7 +167,7 @@ export const UserList = (props: ListProps) => (
|
|||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
filters={userFilters}
|
filters={userFilters}
|
||||||
filterDefaultValues={{ guests: true, deactivated: false, locked: false }}
|
filterDefaultValues={{ guests: true, deactivated: false, locked: false, suspended: false }}
|
||||||
sort={{ field: "name", order: "ASC" }}
|
sort={{ field: "name", order: "ASC" }}
|
||||||
actions={<UserListActions />}
|
actions={<UserListActions />}
|
||||||
pagination={<UserPagination />}
|
pagination={<UserPagination />}
|
||||||
@@ -179,6 +184,7 @@ export const UserList = (props: ListProps) => (
|
|||||||
<BooleanField source="admin" label="resources.users.fields.admin" />
|
<BooleanField source="admin" label="resources.users.fields.admin" />
|
||||||
<BooleanField source="deactivated" label="resources.users.fields.deactivated" />
|
<BooleanField source="deactivated" label="resources.users.fields.deactivated" />
|
||||||
<BooleanField source="locked" label="resources.users.fields.locked" />
|
<BooleanField source="locked" label="resources.users.fields.locked" />
|
||||||
|
<BooleanField source="suspended" label="resources.users.fields.suspended" />
|
||||||
<BooleanField source="erased" sortable={false} label="resources.users.fields.erased" />
|
<BooleanField source="erased" sortable={false} label="resources.users.fields.erased" />
|
||||||
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
|
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||||
</DatagridConfigurable>
|
</DatagridConfigurable>
|
||||||
@@ -189,7 +195,7 @@ export const UserList = (props: ListProps) => (
|
|||||||
// here only local part of user_id
|
// here only local part of user_id
|
||||||
// maxLength = 255 - "@" - ":" - storage.getItem("home_server").length
|
// maxLength = 255 - "@" - ":" - storage.getItem("home_server").length
|
||||||
// storage.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)];
|
||||||
|
|
||||||
@@ -433,6 +439,7 @@ export const UserEdit = (props: EditProps) => {
|
|||||||
<BooleanInput source="admin" />
|
<BooleanInput source="admin" />
|
||||||
<UserBooleanInput source="locked" />
|
<UserBooleanInput source="locked" />
|
||||||
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
||||||
|
<UserBooleanInput source="suspended" helperText="resources.users.helper.suspend" />
|
||||||
<BooleanInput source="erased" disabled />
|
<BooleanInput source="erased" disabled />
|
||||||
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||||
<TextField source="consent_version" />
|
<TextField source="consent_version" />
|
||||||
@@ -493,13 +500,13 @@ export const UserEdit = (props: EditProps) => {
|
|||||||
perPage={50}
|
perPage={50}
|
||||||
sort={{ field: "created_ts", order: "DESC" }}
|
sort={{ field: "created_ts", order: "DESC" }}
|
||||||
>
|
>
|
||||||
<Datagrid sx={{ width: "100%" }}>
|
<Datagrid sx={{ width: "100%" }} bulkActionButtons={<BulkDeleteButton/>}>
|
||||||
<MediaIDField source="media_id" />
|
<MediaIDField source="media_id" />
|
||||||
<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" sx={{ display: "block", width: 200, wordBreak: "break-word" }} />
|
<TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} />
|
||||||
<FunctionField source="upload_name" render={record => decodeURIComponent(record.upload_name)} />
|
<FunctionField source="upload_name" render={record => record.upload_name ? decodeURLComponent(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" />
|
||||||
@@ -521,7 +528,9 @@ export const UserEdit = (props: EditProps) => {
|
|||||||
<ReferenceField reference="rooms" source="id" label="resources.rooms.fields.joined_members" link={false} sortable={false}>
|
<ReferenceField reference="rooms" source="id" label="resources.rooms.fields.joined_members" link={false} sortable={false}>
|
||||||
<TextField source="joined_members" sortable={false} />
|
<TextField source="joined_members" sortable={false} />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
|
<ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}>
|
||||||
<MakeAdminBtn />
|
<MakeAdminBtn />
|
||||||
|
</ReferenceField>
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</ReferenceManyField>
|
</ReferenceManyField>
|
||||||
</FormTab>
|
</FormTab>
|
||||||
@@ -552,6 +561,10 @@ export const UserEdit = (props: EditProps) => {
|
|||||||
<FormTab label="synapseadmin.users.tabs.limits" icon={<LockClockIcon />} path="limits">
|
<FormTab label="synapseadmin.users.tabs.limits" icon={<LockClockIcon />} path="limits">
|
||||||
<UserRateLimits />
|
<UserRateLimits />
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
|
<FormTab label="synapseadmin.users.tabs.account_data" icon={<DocumentScannerIcon />} path="accountdata">
|
||||||
|
<UserAccountData />
|
||||||
|
</FormTab>
|
||||||
</TabbedForm>
|
</TabbedForm>
|
||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
|
@@ -3,6 +3,7 @@ import { AuthProvider, HttpError, Options, fetchUtils } from "react-admin";
|
|||||||
import { MatrixError, displayError } from "../utils/error";
|
import { MatrixError, displayError } from "../utils/error";
|
||||||
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
||||||
import { FetchConfig, ClearConfig } from "../utils/config";
|
import { FetchConfig, ClearConfig } from "../utils/config";
|
||||||
|
import decodeURLComponent from "../utils/decodeURLComponent";
|
||||||
|
|
||||||
const authProvider: AuthProvider = {
|
const authProvider: AuthProvider = {
|
||||||
// called when the user attempts to log in
|
// called when the user attempts to log in
|
||||||
@@ -57,7 +58,7 @@ const authProvider: AuthProvider = {
|
|||||||
base_url = base_url.replace(/\/+$/g, "");
|
base_url = base_url.replace(/\/+$/g, "");
|
||||||
localStorage.setItem("base_url", base_url);
|
localStorage.setItem("base_url", base_url);
|
||||||
|
|
||||||
const decoded_base_url = window.decodeURIComponent(base_url);
|
const decoded_base_url = decodeURLComponent(base_url);
|
||||||
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/v3/login");
|
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/v3/login");
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
@@ -80,11 +81,7 @@ const authProvider: AuthProvider = {
|
|||||||
localStorage.setItem("access_token", accessToken ? accessToken : json.access_token);
|
localStorage.setItem("access_token", accessToken ? accessToken : json.access_token);
|
||||||
localStorage.setItem("device_id", json.device_id);
|
localStorage.setItem("device_id", json.device_id);
|
||||||
localStorage.setItem("login_type", accessToken ? "accessToken" : "credentials");
|
localStorage.setItem("login_type", accessToken ? "accessToken" : "credentials");
|
||||||
|
|
||||||
// when doing access token auth, config is not fetched, so we need to do it here
|
|
||||||
if (accessToken) {
|
|
||||||
await FetchConfig();
|
await FetchConfig();
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve({redirectTo: "/"});
|
return Promise.resolve({redirectTo: "/"});
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
|
@@ -138,6 +138,7 @@ export interface User {
|
|||||||
external_ids: ExternalId[];
|
external_ids: ExternalId[];
|
||||||
user_type?: string;
|
user_type?: string;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
|
suspended?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
@@ -259,6 +260,17 @@ export interface RateLimitsModel {
|
|||||||
burst_count?: number;
|
burst_count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AccountDataModel {
|
||||||
|
account_data: {
|
||||||
|
global: {
|
||||||
|
[key: string]: object;
|
||||||
|
},
|
||||||
|
rooms: {
|
||||||
|
[key: string]: object;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface UsernameAvailabilityResult {
|
export interface UsernameAvailabilityResult {
|
||||||
available?: boolean;
|
available?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -290,16 +302,31 @@ export interface ServerProcessResponse {
|
|||||||
command?: string;
|
command?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServerNotification {
|
||||||
|
event_id: string;
|
||||||
|
output: string;
|
||||||
|
sent_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerNotificationsResponse {
|
||||||
|
success: boolean;
|
||||||
|
notifications: ServerNotification[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface SynapseDataProvider extends DataProvider {
|
export interface SynapseDataProvider extends DataProvider {
|
||||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
|
purgeRemoteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
||||||
updateFeatures: (id: Identifier, features: ExperimentalFeaturesModel) => Promise<void>;
|
updateFeatures: (id: Identifier, features: ExperimentalFeaturesModel) => Promise<void>;
|
||||||
getRateLimits: (id: Identifier) => Promise<RateLimitsModel>;
|
getRateLimits: (id: Identifier) => Promise<RateLimitsModel>;
|
||||||
setRateLimits: (id: Identifier, rateLimits: RateLimitsModel) => Promise<void>;
|
setRateLimits: (id: Identifier, rateLimits: RateLimitsModel) => Promise<void>;
|
||||||
|
getAccountData: (id: Identifier) => Promise<AccountDataModel>;
|
||||||
checkUsernameAvailability: (username: string) => Promise<UsernameAvailabilityResult>;
|
checkUsernameAvailability: (username: string) => Promise<UsernameAvailabilityResult>;
|
||||||
makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
||||||
getServerRunningProcess: (etkeAdminUrl: string) => Promise<ServerProcessResponse>;
|
getServerRunningProcess: (etkeAdminUrl: string) => Promise<ServerProcessResponse>;
|
||||||
getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>;
|
getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>;
|
||||||
|
getServerNotifications: (etkeAdminUrl: string) => Promise<ServerNotificationsResponse>;
|
||||||
|
deleteServerNotifications: (etkeAdminUrl: string) => Promise<{ success: boolean }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceMap = {
|
const resourceMap = {
|
||||||
@@ -384,6 +411,20 @@ const resourceMap = {
|
|||||||
data: "members",
|
data: "members",
|
||||||
total: json => json.total,
|
total: json => json.total,
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
map: (mediaId: string) => ({
|
||||||
|
id: mediaId.replace("mxc://" + localStorage.getItem("home_server") + "/", ""),
|
||||||
|
media_id: mediaId.replace("mxc://" + localStorage.getItem("home_server") + "/", ""),
|
||||||
|
}),
|
||||||
|
reference: (id: Identifier) => ({
|
||||||
|
endpoint: `/_synapse/admin/v1/room/${id}/media`,
|
||||||
|
}),
|
||||||
|
total: json => json.total,
|
||||||
|
data: "local",
|
||||||
|
delete: (params: DeleteParams) => ({
|
||||||
|
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem("home_server")}/${params.id}`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
room_state: {
|
room_state: {
|
||||||
map: (rs: RoomState) => ({
|
map: (rs: RoomState) => ({
|
||||||
...rs,
|
...rs,
|
||||||
@@ -575,7 +616,7 @@ function getSearchOrder(order: "ASC" | "DESC") {
|
|||||||
const baseDataProvider: SynapseDataProvider = {
|
const baseDataProvider: SynapseDataProvider = {
|
||||||
getList: async (resource, params) => {
|
getList: async (resource, params) => {
|
||||||
console.log("getList " + resource);
|
console.log("getList " + resource);
|
||||||
const { user_id, name, guests, deactivated, locked, search_term, destination, valid } = params.filter;
|
const { user_id, name, guests, deactivated, locked, suspended, search_term, destination, valid } = params.filter;
|
||||||
const { page, perPage } = params.pagination as PaginationPayload;
|
const { page, perPage } = params.pagination as PaginationPayload;
|
||||||
const { field, order } = params.sort as SortPayload;
|
const { field, order } = params.sort as SortPayload;
|
||||||
const from = (page - 1) * perPage;
|
const from = (page - 1) * perPage;
|
||||||
@@ -589,6 +630,7 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
guests: guests,
|
guests: guests,
|
||||||
deactivated: deactivated,
|
deactivated: deactivated,
|
||||||
locked: locked,
|
locked: locked,
|
||||||
|
suspended: suspended,
|
||||||
valid: valid,
|
valid: valid,
|
||||||
order_by: field,
|
order_by: field,
|
||||||
dir: getSearchOrder(order),
|
dir: getSearchOrder(order),
|
||||||
@@ -833,6 +875,23 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
return json as DeleteMediaResult;
|
return json as DeleteMediaResult;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purge remote media by date
|
||||||
|
*
|
||||||
|
* @link https://element-hq.github.io/synapse/latest/admin_api/media_admin_api.html#purge-remote-media-api
|
||||||
|
*
|
||||||
|
* @param before_ts Unix timestamp in milliseconds. Files that were last used before this timestamp will be deleted. It is the timestamp of last access, not the timestamp when the file was created.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
purgeRemoteMedia: async ({ before_ts }) => {
|
||||||
|
const endpoint = `/_synapse/admin/v1/purge_media_cache?before_ts=${before_ts}`;
|
||||||
|
|
||||||
|
const base_url = localStorage.getItem("base_url");
|
||||||
|
const endpoint_url = base_url + endpoint;
|
||||||
|
const { json } = await jsonClient(endpoint_url, { method: "POST" });
|
||||||
|
return json as DeleteMediaResult;
|
||||||
|
},
|
||||||
|
|
||||||
uploadMedia: async ({ file, filename, content_type }: UploadMediaParams) => {
|
uploadMedia: async ({ file, filename, content_type }: UploadMediaParams) => {
|
||||||
const base_url = localStorage.getItem("base_url");
|
const base_url = localStorage.getItem("base_url");
|
||||||
const uploadMediaURL = `${base_url}/_matrix/media/v3/upload`;
|
const uploadMediaURL = `${base_url}/_matrix/media/v3/upload`;
|
||||||
@@ -864,6 +923,12 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
const { json } = await jsonClient(endpoint_url);
|
const { json } = await jsonClient(endpoint_url);
|
||||||
return json as RateLimitsModel;
|
return json as RateLimitsModel;
|
||||||
},
|
},
|
||||||
|
getAccountData: async (id: Identifier) => {
|
||||||
|
const base_url = localStorage.getItem("base_url");
|
||||||
|
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/accountdata`;
|
||||||
|
const { json } = await jsonClient(endpoint_url);
|
||||||
|
return json as AccountDataModel;
|
||||||
|
},
|
||||||
setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => {
|
setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => {
|
||||||
const filtered = Object.entries(rateLimits).
|
const filtered = Object.entries(rateLimits).
|
||||||
filter(([key, value]) => value !== null && value !== undefined).
|
filter(([key, value]) => value !== null && value !== undefined).
|
||||||
@@ -961,6 +1026,60 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, ok: false, host: "", results: [] };
|
return { success: false, ok: false, host: "", results: [] };
|
||||||
|
},
|
||||||
|
getServerNotifications: async (serverNotificationsUrl: string): Promise<ServerNotificationsResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${serverNotificationsUrl}/notifications`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error getting server notifications: ${response.status} ${response.statusText}`);
|
||||||
|
return { success: false, notifications: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = response.status;
|
||||||
|
if (status === 204) {
|
||||||
|
return { success: true, notifications: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 200) {
|
||||||
|
const json = await response.json();
|
||||||
|
const result = { success: true, notifications: json } as ServerNotificationsResponse;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, notifications: [] };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting server notifications", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, notifications: [] };
|
||||||
|
},
|
||||||
|
deleteServerNotifications: async (serverNotificationsUrl: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${serverNotificationsUrl}/notifications`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||||
|
},
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error deleting server notifications: ${response.status} ${response.statusText}`);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = response.status;
|
||||||
|
if (status === 204) {
|
||||||
|
const result = { success: true }
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting server notifications", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -22,32 +22,43 @@ let config: Config = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const FetchConfig = async () => {
|
export const FetchConfig = async () => {
|
||||||
|
// load config.json and honor vite base url (import.meta.env.BASE_URL)
|
||||||
|
// if that url doesn't have a trailing slash - add it
|
||||||
|
let configJSONUrl = "config.json"
|
||||||
|
if (import.meta.env.BASE_URL) {
|
||||||
|
configJSONUrl = `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}config.json`;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("config.json");
|
const resp = await fetch(configJSONUrl);
|
||||||
const configJSON = await resp.json();
|
const configJSON = await resp.json();
|
||||||
console.log("Loaded config.json", configJSON);
|
console.log("Loaded", configJSONUrl, configJSON);
|
||||||
LoadConfig(configJSON);
|
LoadConfig(configJSON);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let protocol = "https";
|
||||||
|
const baseURL = localStorage.getItem("base_url");
|
||||||
|
if (baseURL && baseURL.startsWith("http://")) {
|
||||||
|
protocol = "http";
|
||||||
|
}
|
||||||
|
|
||||||
// if home_server is set, try to load https://home_server/.well-known/matrix/client
|
// if home_server is set, try to load https://home_server/.well-known/matrix/client
|
||||||
const homeserver = localStorage.getItem("home_server");
|
const homeserver = localStorage.getItem("home_server");
|
||||||
if (homeserver) {
|
if (homeserver) {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`https://${homeserver}/.well-known/matrix/client`);
|
const resp = await fetch(`${protocol}://${homeserver}/.well-known/matrix/client`);
|
||||||
const configWK = await resp.json();
|
const configWK = await resp.json();
|
||||||
if (!configWK[WellKnownKey]) {
|
if (!configWK[WellKnownKey]) {
|
||||||
console.log(`Loaded https://${homeserver}.well-known/matrix/client, but it doesn't contain ${WellKnownKey} key, skipping`, configWK);
|
console.log(`Loaded ${protocol}://${homeserver}.well-known/matrix/client, but it doesn't contain ${WellKnownKey} key, skipping`, configWK);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Loaded https://${homeserver}.well-known/matrix/client`, configWK);
|
console.log(`Loaded ${protocol}://${homeserver}.well-known/matrix/client`, configWK);
|
||||||
LoadConfig(configWK[WellKnownKey]);
|
LoadConfig(configWK[WellKnownKey]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`https://${homeserver}/.well-known/matrix/client not found, skipping`, e);
|
console.log(`${protocol}://${homeserver}/.well-known/matrix/client not found, skipping`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// load config from context
|
// load config from context
|
||||||
|
15
src/utils/decodeURLComponent.ts
Normal file
15
src/utils/decodeURLComponent.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Decode a URI component, and if it fails, return the original string.
|
||||||
|
* @param str The string to decode.
|
||||||
|
* @returns The decoded string, or the original string if decoding fails.
|
||||||
|
* @example decodeURIComponent("Hello%20World") // "Hello World"
|
||||||
|
*/
|
||||||
|
const decodeURLComponent = (str: any): any => {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(str);
|
||||||
|
} catch (e) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default decodeURLComponent;
|
@@ -16,7 +16,11 @@ export const isMXID = (id: string | Identifier): boolean => mxidPattern.test(id
|
|||||||
* @returns Whether the user is managed by an application service
|
* @returns Whether the user is managed by an application service
|
||||||
*/
|
*/
|
||||||
export const isASManaged = (id: string | Identifier): boolean => {
|
export const isASManaged = (id: string | Identifier): boolean => {
|
||||||
return GetConfig().asManagedUsers.some(regex => regex.test(id as string));
|
const managedUsers = GetConfig().asManagedUsers;
|
||||||
|
if (!managedUsers) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return managedUsers.some(regex => regex.test(id as string));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,15 +42,20 @@ export function generateRandomMXID(): string {
|
|||||||
* @returns full MXID as string
|
* @returns full MXID as string
|
||||||
*/
|
*/
|
||||||
export function returnMXID(input: string | Identifier): string {
|
export function returnMXID(input: string | Identifier): string {
|
||||||
const homeserver = localStorage.getItem("home_server");
|
const inputStr = input as string;
|
||||||
|
const homeserver = localStorage.getItem("home_server") || "";
|
||||||
|
|
||||||
|
// when homeserver is not (just) a domain name, but a domain:port or even an IPv6 address
|
||||||
|
if (homeserver != "" && inputStr.endsWith(homeserver) && inputStr.startsWith("@")) {
|
||||||
|
return inputStr; // Already a valid MXID
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
||||||
const mxidPattern = /^@[^@:]+:[^@:]+$/;
|
|
||||||
if (isMXID(input)) {
|
if (isMXID(input)) {
|
||||||
return input as string; // Already a valid MXID
|
return inputStr; // Already a valid MXID
|
||||||
}
|
}
|
||||||
|
|
||||||
// If input is not a valid MXID, assume it's a localpart and construct the MXID
|
// If input is not a valid MXID, assume it's a localpart and construct the MXID
|
||||||
const localpart = typeof input === 'string' && input.startsWith('@') ? input.slice(1) : input;
|
const localpart = typeof input === 'string' && inputStr.startsWith('@') ? inputStr.slice(1) : inputStr;
|
||||||
return `@${localpart}:${homeserver}`;
|
return `@${localpart}:${homeserver}`;
|
||||||
}
|
}
|
||||||
|
9
testdata/element/config.json
vendored
Normal file
9
testdata/element/config.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"default_hs_url": "http://localhost:8008",
|
||||||
|
"default_is_url": "https://vector.im",
|
||||||
|
"integrations_ui_url": "https://scalar.vector.im/",
|
||||||
|
"integrations_rest_url": "https://scalar.vector.im/api",
|
||||||
|
"bug_report_endpoint_url": "https://riot.im/bugreports/submit",
|
||||||
|
"enableLabs": true
|
||||||
|
}
|
||||||
|
|
34
testdata/element/nginx.conf
vendored
Normal file
34
testdata/element/nginx.conf
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
worker_processes 1;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /tmp/nginx.pid;
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
http {
|
||||||
|
client_body_temp_path /tmp/client_body_temp;
|
||||||
|
proxy_temp_path /tmp/proxy_temp;
|
||||||
|
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||||
|
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||||
|
scgi_temp_path /tmp/scgi_temp;
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name localhost;
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
}
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
testdata/synapse/homeserver.yaml
vendored
4
testdata/synapse/homeserver.yaml
vendored
@@ -78,7 +78,7 @@ password_config:
|
|||||||
pid_file: /homeserver.pid
|
pid_file: /homeserver.pid
|
||||||
presence:
|
presence:
|
||||||
enabled: true
|
enabled: true
|
||||||
public_baseurl: http://synapse:8008/
|
public_baseurl: http://localhost:8008/
|
||||||
push:
|
push:
|
||||||
include_content: true
|
include_content: true
|
||||||
rc_admin_redaction:
|
rc_admin_redaction:
|
||||||
@@ -149,7 +149,7 @@ saml2_config:
|
|||||||
sp_config: null
|
sp_config: null
|
||||||
user_mapping_provider:
|
user_mapping_provider:
|
||||||
config: null
|
config: null
|
||||||
server_name: synapse
|
server_name: synapse:8008
|
||||||
signing_key_path: /config/synapse.signing.key
|
signing_key_path: /config/synapse.signing.key
|
||||||
spam_checker: []
|
spam_checker: []
|
||||||
sso: null
|
sso: null
|
||||||
|
Reference in New Issue
Block a user