mirror of
				https://github.com/UA-Fediland/synapse-admin.git
				synced 2025-11-03 23:38:29 +00:00 
			
		
		
		
	Compare commits
	
		
			19 commits
		
	
	
		
			b112689b8c
			...
			b5ca951b32
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
							 | 
						b5ca951b32 | ||
| 
							 | 
						fac09cb9bb | ||
| 
							 | 
						c9f5360779 | ||
| 
							 | 
						5c492a2ecf | ||
| 
							 | 
						107d60704b | ||
| 
							 | 
						5b50838fb7 | ||
| 
							 | 
						9da953e78a | ||
| 
							 | 
						91af5068c0 | ||
| 
							 | 
						ce1d806818 | ||
| 
							 | 
						211e6e6915 | ||
| 
							 | 
						3ae4dcffab | ||
| 
							 | 
						7061c5cbff | ||
| 
							 | 
						6fe8ab3115 | ||
| 
							 | 
						04243eefa9 | ||
| 
							 | 
						4761ea36bc | ||
| 
							 | 
						72f5ab937e | ||
| 
							 | 
						39dd6617de | ||
| 
							 | 
						2466af6936 | ||
| 
							 | 
						03fcd8126a | 
					 53 changed files with 2329 additions and 3555 deletions
				
			
		
							
								
								
									
										13
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					# EditorConfig https://EditorConfig.org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# top-most EditorConfig file
 | 
				
			||||||
 | 
					root = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[*]
 | 
				
			||||||
 | 
					charset = utf-8
 | 
				
			||||||
 | 
					end_of_line = lf
 | 
				
			||||||
 | 
					indent_size = 2
 | 
				
			||||||
 | 
					indent_style = space
 | 
				
			||||||
 | 
					insert_final_newline = true
 | 
				
			||||||
 | 
					max_line_length = 120
 | 
				
			||||||
 | 
					trim_trailing_whitespace = true
 | 
				
			||||||
							
								
								
									
										2
									
								
								.github/workflows/build-test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/build-test.yml
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -17,5 +17,7 @@ jobs:
 | 
				
			||||||
          node-version: "18"
 | 
					          node-version: "18"
 | 
				
			||||||
      - name: Install dependencies
 | 
					      - name: Install dependencies
 | 
				
			||||||
        run: yarn --immutable
 | 
					        run: yarn --immutable
 | 
				
			||||||
 | 
					      - name: Run checks
 | 
				
			||||||
 | 
					        run: yarn lint
 | 
				
			||||||
      - name: Run tests
 | 
					      - name: Run tests
 | 
				
			||||||
        run: yarn test
 | 
					        run: yarn test
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1,2 @@
 | 
				
			||||||
 | 
					.vscode
 | 
				
			||||||
.yarn
 | 
					.yarn
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										11
									
								
								.prettierrc
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								.prettierrc
									
									
									
									
									
								
							| 
						 | 
					@ -1,11 +0,0 @@
 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  "printWidth": 80,
 | 
					 | 
				
			||||||
  "tabWidth": 2,
 | 
					 | 
				
			||||||
  "useTabs": false,
 | 
					 | 
				
			||||||
  "semi": true,
 | 
					 | 
				
			||||||
  "singleQuote": false,
 | 
					 | 
				
			||||||
  "trailingComma": "es5",
 | 
					 | 
				
			||||||
  "bracketSpacing": true,
 | 
					 | 
				
			||||||
  "bracketSameLine": false,
 | 
					 | 
				
			||||||
  "arrowParens": "avoid"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
								
							| 
						 | 
					@ -13,7 +13,7 @@ This project is built using [react-admin](https://marmelab.com/react-admin/).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Supported Synapse
 | 
					### Supported Synapse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
It needs at least [Synapse](https://github.com/element-hq/synapse) v1.52.0 for all functions to work as expected!
 | 
					It needs at least [Synapse](https://github.com/element-hq/synapse) v1.93.0 for all functions to work as expected!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You get your server version with the request `/_synapse/admin/v1/server_version`.
 | 
					You get your server version with the request `/_synapse/admin/v1/server_version`.
 | 
				
			||||||
See also [Synapse version API](https://element-hq.github.io/synapse/latest/admin_api/version_api.html).
 | 
					See also [Synapse version API](https://element-hq.github.io/synapse/latest/admin_api/version_api.html).
 | 
				
			||||||
| 
						 | 
					@ -104,10 +104,7 @@ or to a list of homeservers:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```json
 | 
					```json
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  "restrictBaseUrl": [
 | 
					  "restrictBaseUrl": ["https://your-first-matrix-server.example.com", "https://your-second-matrix-server.example.com"]
 | 
				
			||||||
    "https://your-first-matrix-server.example.com",
 | 
					 | 
				
			||||||
    "https://your-second-matrix-server.example.com"
 | 
					 | 
				
			||||||
  ]
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -166,5 +163,6 @@ services:
 | 
				
			||||||
## Development
 | 
					## Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- See https://yarnpkg.com/getting-started/editor-sdks how to setup your IDE
 | 
					- See https://yarnpkg.com/getting-started/editor-sdks how to setup your IDE
 | 
				
			||||||
- Use `yarn test` to run all style, lint and unit tests
 | 
					- Use `yarn lint` to run all style and linter checks
 | 
				
			||||||
 | 
					- Use `yarn test` to run all unit tests
 | 
				
			||||||
- Use `yarn fix` to fix the coding style
 | 
					- Use `yarn fix` to fix the coding style
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -119,7 +119,7 @@
 | 
				
			||||||
        <div class="loader">Loading...</div>
 | 
					        <div class="loader">Loading...</div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <script type="module" src="/src/index.jsx"></script>
 | 
					    <script type="module" src="/src/index.tsx"></script>
 | 
				
			||||||
    <footer
 | 
					    <footer
 | 
				
			||||||
      style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
 | 
					      style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
 | 
				
			||||||
      <a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
 | 
					      <a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										13
									
								
								jest.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								jest.config.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					import type { JestConfigWithTsJest } from "ts-jest";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const config: JestConfigWithTsJest = {
 | 
				
			||||||
 | 
					  preset: "ts-jest",
 | 
				
			||||||
 | 
					  testEnvironment: "jsdom",
 | 
				
			||||||
 | 
					  collectCoverage: true,
 | 
				
			||||||
 | 
					  coveragePathIgnorePatterns: ["node_modules", "dist"],
 | 
				
			||||||
 | 
					  coverageDirectory: "<rootDir>/coverage/",
 | 
				
			||||||
 | 
					  coverageReporters: ["html", "text", "text-summary", "cobertura"],
 | 
				
			||||||
 | 
					  extensionsToTreatAsEsm: [".ts", ".tsx"],
 | 
				
			||||||
 | 
					  setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export default config;
 | 
				
			||||||
							
								
								
									
										142
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										142
									
								
								package.json
									
									
									
									
									
								
							| 
						 | 
					@ -12,24 +12,35 @@
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "packageManager": "yarn@4.1.1",
 | 
					  "packageManager": "yarn@4.1.1",
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@babel/core": "^7.24.4",
 | 
					    "@eslint/js": "^9.1.1",
 | 
				
			||||||
    "@babel/preset-env": "^7.24.4",
 | 
					 | 
				
			||||||
    "@babel/preset-react": "^7.24.1",
 | 
					 | 
				
			||||||
    "@testing-library/dom": "^10.0.0",
 | 
					    "@testing-library/dom": "^10.0.0",
 | 
				
			||||||
    "@testing-library/jest-dom": "^6.0.0",
 | 
					    "@testing-library/jest-dom": "^6.0.0",
 | 
				
			||||||
    "@testing-library/react": "^15.0.2",
 | 
					    "@testing-library/react": "^15.0.2",
 | 
				
			||||||
    "@testing-library/user-event": "^14.5.2",
 | 
					    "@testing-library/user-event": "^14.5.2",
 | 
				
			||||||
 | 
					    "@types/jest": "^29.5.12",
 | 
				
			||||||
 | 
					    "@types/lodash": "^4.17.0",
 | 
				
			||||||
 | 
					    "@types/node": "^20.12.7",
 | 
				
			||||||
 | 
					    "@types/papaparse": "^5.3.14",
 | 
				
			||||||
 | 
					    "@types/react": "^18.3.1",
 | 
				
			||||||
 | 
					    "@typescript-eslint/eslint-plugin": "^7.7.1",
 | 
				
			||||||
 | 
					    "@typescript-eslint/parser": "^7.8.0",
 | 
				
			||||||
    "@vitejs/plugin-react": "^4.0.0",
 | 
					    "@vitejs/plugin-react": "^4.0.0",
 | 
				
			||||||
    "babel-jest": "^29.7.0",
 | 
					 | 
				
			||||||
    "eslint": "^8.57.0",
 | 
					    "eslint": "^8.57.0",
 | 
				
			||||||
    "eslint-config-prettier": "^9.1.0",
 | 
					    "eslint-config-prettier": "^9.1.0",
 | 
				
			||||||
    "eslint-config-react-app": "^7.0.1",
 | 
					    "eslint-plugin-import": "^2.29.1",
 | 
				
			||||||
 | 
					    "eslint-plugin-jsx-a11y": "^6.8.0",
 | 
				
			||||||
    "eslint-plugin-prettier": "^5.1.3",
 | 
					    "eslint-plugin-prettier": "^5.1.3",
 | 
				
			||||||
 | 
					    "eslint-plugin-unused-imports": "^3.2.0",
 | 
				
			||||||
 | 
					    "eslint-plugin-yaml": "^0.5.0",
 | 
				
			||||||
    "jest": "^29.7.0",
 | 
					    "jest": "^29.7.0",
 | 
				
			||||||
    "jest-environment-jsdom": "^29.7.0",
 | 
					    "jest-environment-jsdom": "^29.7.0",
 | 
				
			||||||
    "jest-fetch-mock": "^3.0.3",
 | 
					    "jest-fetch-mock": "^3.0.3",
 | 
				
			||||||
    "prettier": "^3.2.5",
 | 
					    "prettier": "^3.2.5",
 | 
				
			||||||
    "react-test-renderer": "^18.2.0",
 | 
					    "react-test-renderer": "^18.3.1",
 | 
				
			||||||
 | 
					    "ts-jest": "^29.1.2",
 | 
				
			||||||
 | 
					    "ts-node": "^10.9.2",
 | 
				
			||||||
 | 
					    "typescript": "^5.4.5",
 | 
				
			||||||
 | 
					    "typescript-eslint": "^7.8.0",
 | 
				
			||||||
    "vite": "^5.0.0",
 | 
					    "vite": "^5.0.0",
 | 
				
			||||||
    "vite-plugin-version-mark": "^0.0.13"
 | 
					    "vite-plugin-version-mark": "^0.0.13"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -38,58 +49,103 @@
 | 
				
			||||||
    "@emotion/styled": "^11.3.0",
 | 
					    "@emotion/styled": "^11.3.0",
 | 
				
			||||||
    "@haleos/ra-language-german": "^1.0.0",
 | 
					    "@haleos/ra-language-german": "^1.0.0",
 | 
				
			||||||
    "@haxqer/ra-language-chinese": "^4.16.2",
 | 
					    "@haxqer/ra-language-chinese": "^4.16.2",
 | 
				
			||||||
    "@mui/icons-material": "^5.15.15",
 | 
					    "@mui/icons-material": "^5.15.16",
 | 
				
			||||||
    "@mui/material": "^5.15.15",
 | 
					    "@mui/material": "^5.15.16",
 | 
				
			||||||
    "history": "^5.1.0",
 | 
					    "history": "^5.1.0",
 | 
				
			||||||
    "lodash": "^4.17.21",
 | 
					    "lodash": "^4.17.21",
 | 
				
			||||||
    "papaparse": "^5.4.1",
 | 
					    "papaparse": "^5.4.1",
 | 
				
			||||||
    "query-string": "^7.1.1",
 | 
					    "query-string": "^7.1.1",
 | 
				
			||||||
    "ra-core": "^4.16.15",
 | 
					    "ra-core": "^4.16.17",
 | 
				
			||||||
    "ra-i18n-polyglot": "^4.16.15",
 | 
					    "ra-i18n-polyglot": "^4.16.17",
 | 
				
			||||||
    "ra-language-english": "^4.16.15",
 | 
					    "ra-language-english": "^4.16.17",
 | 
				
			||||||
    "ra-language-farsi": "^4.2.0",
 | 
					    "ra-language-farsi": "^4.2.0",
 | 
				
			||||||
    "ra-language-french": "^4.16.15",
 | 
					    "ra-language-french": "^4.16.17",
 | 
				
			||||||
    "ra-language-italian": "^3.13.1",
 | 
					    "ra-language-italian": "^3.13.1",
 | 
				
			||||||
    "react": "^18.0.0",
 | 
					    "react": "^18.3.1",
 | 
				
			||||||
    "react-admin": "^4.16.15",
 | 
					    "react-admin": "^4.16.17",
 | 
				
			||||||
    "react-dom": "^18.0.0",
 | 
					    "react-dom": "^18.3.1",
 | 
				
			||||||
    "react-hook-form": "^7.43.9",
 | 
					    "react-hook-form": "^7.43.9",
 | 
				
			||||||
    "react-is": "^18.2.0",
 | 
					    "react-is": "^18.3.1",
 | 
				
			||||||
    "react-query": "^3.32.1",
 | 
					    "react-query": "^3.32.1",
 | 
				
			||||||
    "react-router": "^6.1.0",
 | 
					    "react-router": "^6.23.0",
 | 
				
			||||||
    "react-router-dom": "^6.1.0"
 | 
					    "react-router-dom": "^6.23.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "start": "vite serve",
 | 
					    "start": "vite serve",
 | 
				
			||||||
    "build": "vite build",
 | 
					    "build": "vite build",
 | 
				
			||||||
    "fix:other": "yarn prettier --write",
 | 
					    "lint": "eslint --ignore-path .gitignore --ext .ts,.tsx,.yml,.yaml .",
 | 
				
			||||||
    "fix:code": "yarn test:lint --fix",
 | 
					    "fix": "yarn lint --fix",
 | 
				
			||||||
    "fix": "yarn fix:code && yarn fix:other",
 | 
					    "test": "yarn jest",
 | 
				
			||||||
    "prettier": "prettier \"**/*.{js,jsx,json,md,scss,yaml,yml}\"",
 | 
					    "test:watch": "yarn jest --watch"
 | 
				
			||||||
    "test:code": "jest",
 | 
					 | 
				
			||||||
    "test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .",
 | 
					 | 
				
			||||||
    "test:style": "yarn prettier --check",
 | 
					 | 
				
			||||||
    "test": "yarn test:style && yarn test:lint && yarn test:code"
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "babel": {
 | 
					 | 
				
			||||||
    "presets": [
 | 
					 | 
				
			||||||
      "@babel/preset-env",
 | 
					 | 
				
			||||||
      [
 | 
					 | 
				
			||||||
        "@babel/preset-react",
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "runtime": "automatic"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "eslintConfig": {
 | 
					  "eslintConfig": {
 | 
				
			||||||
    "extends": "react-app"
 | 
					    "env": {
 | 
				
			||||||
 | 
					      "browser": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "plugins": [
 | 
				
			||||||
 | 
					      "import",
 | 
				
			||||||
 | 
					      "prettier",
 | 
				
			||||||
 | 
					      "unused-imports",
 | 
				
			||||||
 | 
					      "@typescript-eslint",
 | 
				
			||||||
 | 
					      "yaml"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "extends": [
 | 
				
			||||||
 | 
					      "eslint:recommended",
 | 
				
			||||||
 | 
					      "plugin:@typescript-eslint/recommended",
 | 
				
			||||||
 | 
					      "plugin:@typescript-eslint/stylistic",
 | 
				
			||||||
 | 
					      "plugin:import/typescript",
 | 
				
			||||||
 | 
					      "plugin:yaml/recommended"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "parser": "@typescript-eslint/parser",
 | 
				
			||||||
 | 
					    "parserOptions": {
 | 
				
			||||||
 | 
					      "project": "./tsconfig.eslint.json"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "root": true,
 | 
				
			||||||
 | 
					    "rules": {
 | 
				
			||||||
 | 
					      "prettier/prettier": "error",
 | 
				
			||||||
 | 
					      "import/no-extraneous-dependencies": [
 | 
				
			||||||
 | 
					        "error",
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "devDependencies": [
 | 
				
			||||||
 | 
					            "**/vite.config.ts",
 | 
				
			||||||
 | 
					            "**/jest.setup.ts",
 | 
				
			||||||
 | 
					            "**/*.test.ts",
 | 
				
			||||||
 | 
					            "**/*.test.tsx"
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "import/order": [
 | 
				
			||||||
 | 
					        "error",
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "alphabetize": {
 | 
				
			||||||
 | 
					            "order": "asc",
 | 
				
			||||||
 | 
					            "caseInsensitive": false
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "newlines-between": "always",
 | 
				
			||||||
 | 
					          "groups": [
 | 
				
			||||||
 | 
					            "external",
 | 
				
			||||||
 | 
					            "builtin",
 | 
				
			||||||
 | 
					            "internal",
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					              "parent",
 | 
				
			||||||
 | 
					              "sibling",
 | 
				
			||||||
 | 
					              "index"
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "unused-imports/no-unused-imports-ts": 2
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "jest": {
 | 
					  "prettier": {
 | 
				
			||||||
    "testEnvironment": "jsdom",
 | 
					    "printWidth": 120,
 | 
				
			||||||
    "setupFilesAfterEnv": [
 | 
					    "tabWidth": 2,
 | 
				
			||||||
      "<rootDir>/src/setupTests.js"
 | 
					    "useTabs": false,
 | 
				
			||||||
    ]
 | 
					    "semi": true,
 | 
				
			||||||
 | 
					    "singleQuote": false,
 | 
				
			||||||
 | 
					    "trailingComma": "es5",
 | 
				
			||||||
 | 
					    "bracketSpacing": true,
 | 
				
			||||||
 | 
					    "arrowParens": "avoid"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "browserslist": {
 | 
					  "browserslist": {
 | 
				
			||||||
    "production": [
 | 
					    "production": [
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import React from "react";
 | 
					 | 
				
			||||||
import { render, screen } from "@testing-library/react";
 | 
					import { render, screen } from "@testing-library/react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import App from "./App";
 | 
					import App from "./App";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe("App", () => {
 | 
					describe("App", () => {
 | 
				
			||||||
| 
						 | 
					@ -1,29 +1,25 @@
 | 
				
			||||||
import React from "react";
 | 
					import { merge } from "lodash";
 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  Admin,
 | 
					 | 
				
			||||||
  CustomRoutes,
 | 
					 | 
				
			||||||
  Resource,
 | 
					 | 
				
			||||||
  resolveBrowserLocale,
 | 
					 | 
				
			||||||
} from "react-admin";
 | 
					 | 
				
			||||||
import polyglotI18nProvider from "ra-i18n-polyglot";
 | 
					import polyglotI18nProvider from "ra-i18n-polyglot";
 | 
				
			||||||
import merge from "lodash/merge";
 | 
					
 | 
				
			||||||
import authProvider from "./synapse/authProvider";
 | 
					import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
 | 
				
			||||||
import dataProvider from "./synapse/dataProvider";
 | 
					import { Route } from "react-router-dom";
 | 
				
			||||||
import users from "./components/users";
 | 
					
 | 
				
			||||||
import rooms from "./components/rooms";
 | 
					 | 
				
			||||||
import userMediaStats from "./components/statistics";
 | 
					 | 
				
			||||||
import reports from "./components/EventReports";
 | 
					import reports from "./components/EventReports";
 | 
				
			||||||
 | 
					import { ImportFeature } from "./components/ImportFeature";
 | 
				
			||||||
 | 
					import LoginPage from "./components/LoginPage";
 | 
				
			||||||
 | 
					import registrationToken from "./components/RegistrationTokens";
 | 
				
			||||||
import roomDirectory from "./components/RoomDirectory";
 | 
					import roomDirectory from "./components/RoomDirectory";
 | 
				
			||||||
import destinations from "./components/destinations";
 | 
					import destinations from "./components/destinations";
 | 
				
			||||||
import registrationToken from "./components/RegistrationTokens";
 | 
					import rooms from "./components/rooms";
 | 
				
			||||||
import LoginPage from "./components/LoginPage";
 | 
					import userMediaStats from "./components/statistics";
 | 
				
			||||||
import { ImportFeature } from "./components/ImportFeature";
 | 
					import users from "./components/users";
 | 
				
			||||||
import { Route } from "react-router-dom";
 | 
					 | 
				
			||||||
import germanMessages from "./i18n/de";
 | 
					import germanMessages from "./i18n/de";
 | 
				
			||||||
import englishMessages from "./i18n/en";
 | 
					import englishMessages from "./i18n/en";
 | 
				
			||||||
import frenchMessages from "./i18n/fr";
 | 
					import frenchMessages from "./i18n/fr";
 | 
				
			||||||
import chineseMessages from "./i18n/zh";
 | 
					 | 
				
			||||||
import italianMessages from "./i18n/it";
 | 
					import italianMessages from "./i18n/it";
 | 
				
			||||||
 | 
					import chineseMessages from "./i18n/zh";
 | 
				
			||||||
 | 
					import authProvider from "./synapse/authProvider";
 | 
				
			||||||
 | 
					import dataProvider from "./synapse/dataProvider";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TODO: Can we use lazy loading together with browser locale?
 | 
					// TODO: Can we use lazy loading together with browser locale?
 | 
				
			||||||
const messages = {
 | 
					const messages = {
 | 
				
			||||||
| 
						 | 
					@ -34,8 +30,7 @@ const messages = {
 | 
				
			||||||
  zh: chineseMessages,
 | 
					  zh: chineseMessages,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
const i18nProvider = polyglotI18nProvider(
 | 
					const i18nProvider = polyglotI18nProvider(
 | 
				
			||||||
  locale =>
 | 
					  locale => (messages[locale] ? merge({}, messages.en, messages[locale]) : messages.en),
 | 
				
			||||||
    messages[locale] ? merge({}, messages.en, messages[locale]) : messages.en,
 | 
					 | 
				
			||||||
  resolveBrowserLocale(),
 | 
					  resolveBrowserLocale(),
 | 
				
			||||||
  [
 | 
					  [
 | 
				
			||||||
    { locale: "en", name: "English" },
 | 
					    { locale: "en", name: "English" },
 | 
				
			||||||
| 
						 | 
					@ -1,5 +0,0 @@
 | 
				
			||||||
import { createContext, useContext } from "react";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const AppContext = createContext({});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const useAppContext = () => useContext(AppContext);
 | 
					 | 
				
			||||||
							
								
								
									
										9
									
								
								src/AppContext.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/AppContext.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					import { createContext, useContext } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface AppContextType {
 | 
				
			||||||
 | 
					  restrictBaseUrl: string | string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AppContext = createContext({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useAppContext = () => useContext(AppContext) as AppContextType;
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
import React from "react";
 | 
					 | 
				
			||||||
import { RecordContextProvider } from "react-admin";
 | 
					 | 
				
			||||||
import { render, screen } from "@testing-library/react";
 | 
					import { render, screen } from "@testing-library/react";
 | 
				
			||||||
 | 
					import { RecordContextProvider } from "react-admin";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import AvatarField from "./AvatarField";
 | 
					import AvatarField from "./AvatarField";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe("AvatarField", () => {
 | 
					describe("AvatarField", () => {
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import React from "react";
 | 
					import { get } from "lodash";
 | 
				
			||||||
import get from "lodash/get";
 | 
					
 | 
				
			||||||
import { Avatar } from "@mui/material";
 | 
					import { Avatar } from "@mui/material";
 | 
				
			||||||
import { useRecordContext } from "react-admin";
 | 
					import { useRecordContext } from "react-admin";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,16 +7,7 @@ const AvatarField = ({ source, ...rest }) => {
 | 
				
			||||||
  const record = useRecordContext(rest);
 | 
					  const record = useRecordContext(rest);
 | 
				
			||||||
  const src = get(record, source)?.toString();
 | 
					  const src = get(record, source)?.toString();
 | 
				
			||||||
  const { alt, classes, sizes, sx, variant } = rest;
 | 
					  const { alt, classes, sizes, sx, variant } = rest;
 | 
				
			||||||
  return (
 | 
					  return <Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant} />;
 | 
				
			||||||
    <Avatar
 | 
					 | 
				
			||||||
      alt={alt}
 | 
					 | 
				
			||||||
      classes={classes}
 | 
					 | 
				
			||||||
      sizes={sizes}
 | 
					 | 
				
			||||||
      src={src}
 | 
					 | 
				
			||||||
      sx={sx}
 | 
					 | 
				
			||||||
      variant={variant}
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default AvatarField;
 | 
					export default AvatarField;
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,18 @@
 | 
				
			||||||
import React from "react";
 | 
					import PageviewIcon from "@mui/icons-material/Pageview";
 | 
				
			||||||
 | 
					import ViewListIcon from "@mui/icons-material/ViewList";
 | 
				
			||||||
 | 
					import ReportIcon from "@mui/icons-material/Warning";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Datagrid,
 | 
					  Datagrid,
 | 
				
			||||||
  DateField,
 | 
					  DateField,
 | 
				
			||||||
  DeleteButton,
 | 
					  DeleteButton,
 | 
				
			||||||
  List,
 | 
					  List,
 | 
				
			||||||
 | 
					  ListProps,
 | 
				
			||||||
  NumberField,
 | 
					  NumberField,
 | 
				
			||||||
  Pagination,
 | 
					  Pagination,
 | 
				
			||||||
  ReferenceField,
 | 
					  ReferenceField,
 | 
				
			||||||
 | 
					  ResourceProps,
 | 
				
			||||||
  Show,
 | 
					  Show,
 | 
				
			||||||
 | 
					  ShowProps,
 | 
				
			||||||
  Tab,
 | 
					  Tab,
 | 
				
			||||||
  TabbedShowLayout,
 | 
					  TabbedShowLayout,
 | 
				
			||||||
  TextField,
 | 
					  TextField,
 | 
				
			||||||
| 
						 | 
					@ -15,25 +20,13 @@ import {
 | 
				
			||||||
  useRecordContext,
 | 
					  useRecordContext,
 | 
				
			||||||
  useTranslate,
 | 
					  useTranslate,
 | 
				
			||||||
} from "react-admin";
 | 
					} from "react-admin";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { DATE_FORMAT } from "./date";
 | 
				
			||||||
import { MXCField } from "./media";
 | 
					import { MXCField } from "./media";
 | 
				
			||||||
import PageviewIcon from "@mui/icons-material/Pageview";
 | 
					 | 
				
			||||||
import ReportIcon from "@mui/icons-material/Warning";
 | 
					 | 
				
			||||||
import ViewListIcon from "@mui/icons-material/ViewList";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const date_format = {
 | 
					const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
 | 
				
			||||||
  year: "numeric",
 | 
					 | 
				
			||||||
  month: "2-digit",
 | 
					 | 
				
			||||||
  day: "2-digit",
 | 
					 | 
				
			||||||
  hour: "2-digit",
 | 
					 | 
				
			||||||
  minute: "2-digit",
 | 
					 | 
				
			||||||
  second: "2-digit",
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ReportPagination = () => (
 | 
					export const ReportShow = (props: ShowProps) => {
 | 
				
			||||||
  <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const ReportShow = props => {
 | 
					 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Show {...props} actions={<ReportShowActions />}>
 | 
					    <Show {...props} actions={<ReportShowActions />}>
 | 
				
			||||||
| 
						 | 
					@ -44,43 +37,21 @@ export const ReportShow = props => {
 | 
				
			||||||
          })}
 | 
					          })}
 | 
				
			||||||
          icon={<ViewListIcon />}
 | 
					          icon={<ViewListIcon />}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <DateField
 | 
					          <DateField source="received_ts" showTime options={DATE_FORMAT} sortable={true} />
 | 
				
			||||||
            source="received_ts"
 | 
					 | 
				
			||||||
            showTime
 | 
					 | 
				
			||||||
            options={date_format}
 | 
					 | 
				
			||||||
            sortable={true}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <ReferenceField source="user_id" reference="users">
 | 
					          <ReferenceField source="user_id" reference="users">
 | 
				
			||||||
            <TextField source="id" />
 | 
					            <TextField source="id" />
 | 
				
			||||||
          </ReferenceField>
 | 
					          </ReferenceField>
 | 
				
			||||||
          <NumberField source="score" />
 | 
					          <NumberField source="score" />
 | 
				
			||||||
          <TextField source="reason" />
 | 
					          <TextField source="reason" />
 | 
				
			||||||
          <TextField source="name" />
 | 
					          <TextField source="name" />
 | 
				
			||||||
          <TextField
 | 
					          <TextField source="canonical_alias" label="resources.rooms.fields.canonical_alias" />
 | 
				
			||||||
            source="canonical_alias"
 | 
					          <ReferenceField source="room_id" reference="rooms" link="show" label="resources.rooms.fields.room_id">
 | 
				
			||||||
            label="resources.rooms.fields.canonical_alias"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <ReferenceField
 | 
					 | 
				
			||||||
            source="room_id"
 | 
					 | 
				
			||||||
            reference="rooms"
 | 
					 | 
				
			||||||
            link="show"
 | 
					 | 
				
			||||||
            label="resources.rooms.fields.room_id"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <TextField source="id" />
 | 
					            <TextField source="id" />
 | 
				
			||||||
          </ReferenceField>
 | 
					          </ReferenceField>
 | 
				
			||||||
        </Tab>
 | 
					        </Tab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Tab
 | 
					        <Tab label="synapseadmin.reports.tabs.detail" icon={<PageviewIcon />} path="detail">
 | 
				
			||||||
          label="synapseadmin.reports.tabs.detail"
 | 
					          <DateField source="event_json.origin_server_ts" showTime options={DATE_FORMAT} sortable={true} />
 | 
				
			||||||
          icon={<PageviewIcon />}
 | 
					 | 
				
			||||||
          path="detail"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <DateField
 | 
					 | 
				
			||||||
            source="event_json.origin_server_ts"
 | 
					 | 
				
			||||||
            showTime
 | 
					 | 
				
			||||||
            options={date_format}
 | 
					 | 
				
			||||||
            sortable={true}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <ReferenceField source="sender" reference="users">
 | 
					          <ReferenceField source="sender" reference="users">
 | 
				
			||||||
            <TextField source="id" />
 | 
					            <TextField source="id" />
 | 
				
			||||||
          </ReferenceField>
 | 
					          </ReferenceField>
 | 
				
			||||||
| 
						 | 
					@ -95,10 +66,7 @@ export const ReportShow = props => {
 | 
				
			||||||
          <TextField source="event_json.content.format" />
 | 
					          <TextField source="event_json.content.format" />
 | 
				
			||||||
          <TextField source="event_json.content.formatted_body" />
 | 
					          <TextField source="event_json.content.formatted_body" />
 | 
				
			||||||
          <TextField source="event_json.content.algorithm" />
 | 
					          <TextField source="event_json.content.algorithm" />
 | 
				
			||||||
          <TextField
 | 
					          <TextField source="event_json.content.device_id" label="resources.devices.fields.device_id" />
 | 
				
			||||||
            source="event_json.content.device_id"
 | 
					 | 
				
			||||||
            label="resources.devices.fields.device_id"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </Tab>
 | 
					        </Tab>
 | 
				
			||||||
      </TabbedShowLayout>
 | 
					      </TabbedShowLayout>
 | 
				
			||||||
    </Show>
 | 
					    </Show>
 | 
				
			||||||
| 
						 | 
					@ -120,20 +88,11 @@ const ReportShowActions = () => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ReportList = props => (
 | 
					export const ReportList = (props: ListProps) => (
 | 
				
			||||||
  <List
 | 
					  <List {...props} pagination={<ReportPagination />} sort={{ field: "received_ts", order: "DESC" }}>
 | 
				
			||||||
    {...props}
 | 
					 | 
				
			||||||
    pagination={<ReportPagination />}
 | 
					 | 
				
			||||||
    sort={{ field: "received_ts", order: "DESC" }}
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <Datagrid rowClick="show" bulkActionButtons={false}>
 | 
					    <Datagrid rowClick="show" bulkActionButtons={false}>
 | 
				
			||||||
      <TextField source="id" sortable={false} />
 | 
					      <TextField source="id" sortable={false} />
 | 
				
			||||||
      <DateField
 | 
					      <DateField source="received_ts" showTime options={DATE_FORMAT} sortable={true} />
 | 
				
			||||||
        source="received_ts"
 | 
					 | 
				
			||||||
        showTime
 | 
					 | 
				
			||||||
        options={date_format}
 | 
					 | 
				
			||||||
        sortable={true}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <TextField sortable={false} source="user_id" />
 | 
					      <TextField sortable={false} source="user_id" />
 | 
				
			||||||
      <TextField sortable={false} source="name" />
 | 
					      <TextField sortable={false} source="name" />
 | 
				
			||||||
      <TextField sortable={false} source="score" />
 | 
					      <TextField sortable={false} source="score" />
 | 
				
			||||||
| 
						 | 
					@ -141,7 +100,7 @@ export const ReportList = props => (
 | 
				
			||||||
  </List>
 | 
					  </List>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const resource = {
 | 
					const resource: ResourceProps = {
 | 
				
			||||||
  name: "reports",
 | 
					  name: "reports",
 | 
				
			||||||
  icon: ReportIcon,
 | 
					  icon: ReportIcon,
 | 
				
			||||||
  list: ReportList,
 | 
					  list: ReportList,
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
import React, { useState } from "react";
 | 
					import { parse as parseCsv, unparse as unparseCsv, ParseResult } from "papaparse";
 | 
				
			||||||
import { useDataProvider, useNotify, Title } from "react-admin";
 | 
					import { ChangeEvent, useState } from "react";
 | 
				
			||||||
import { parse as parseCsv, unparse as unparseCsv } from "papaparse";
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
  Card,
 | 
					  Card,
 | 
				
			||||||
| 
						 | 
					@ -12,36 +12,65 @@ import {
 | 
				
			||||||
  FormControlLabel,
 | 
					  FormControlLabel,
 | 
				
			||||||
  NativeSelect,
 | 
					  NativeSelect,
 | 
				
			||||||
} from "@mui/material";
 | 
					} from "@mui/material";
 | 
				
			||||||
import { useTranslate } from "ra-core";
 | 
					import { DataProvider, useTranslate } from "ra-core";
 | 
				
			||||||
import { generateRandomUser } from "./users";
 | 
					import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { generateRandomMxId, generateRandomPassword } from "../synapse/synapse";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LOGGING = true;
 | 
					const LOGGING = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const expectedFields = ["id", "displayname"].sort();
 | 
					const expectedFields = ["id", "displayname"].sort();
 | 
				
			||||||
const optionalFields = [
 | 
					 | 
				
			||||||
  "user_type",
 | 
					 | 
				
			||||||
  "guest",
 | 
					 | 
				
			||||||
  "admin",
 | 
					 | 
				
			||||||
  "deactivated",
 | 
					 | 
				
			||||||
  "avatar_url",
 | 
					 | 
				
			||||||
  "password",
 | 
					 | 
				
			||||||
].sort();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function TranslatableOption({ value, text }) {
 | 
					function TranslatableOption({ value, text }) {
 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
  return <option value={value}>{translate(text)}</option>;
 | 
					  return <option value={value}>{translate(text)}</option>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Progress = {
 | 
				
			||||||
 | 
					  done: number;
 | 
				
			||||||
 | 
					  limit: number;
 | 
				
			||||||
 | 
					} | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ImportLine {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  displayname: string;
 | 
				
			||||||
 | 
					  user_type?: string;
 | 
				
			||||||
 | 
					  name?: string;
 | 
				
			||||||
 | 
					  deactivated?: boolean;
 | 
				
			||||||
 | 
					  guest?: boolean;
 | 
				
			||||||
 | 
					  admin?: boolean;
 | 
				
			||||||
 | 
					  is_admin?: boolean;
 | 
				
			||||||
 | 
					  password?: string;
 | 
				
			||||||
 | 
					  avatar_url?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ChangeStats {
 | 
				
			||||||
 | 
					  total: number;
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  is_guest: number;
 | 
				
			||||||
 | 
					  admin: number;
 | 
				
			||||||
 | 
					  password: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ImportResult {
 | 
				
			||||||
 | 
					  skippedRecords: RaRecord[];
 | 
				
			||||||
 | 
					  erroredRecords: RaRecord[];
 | 
				
			||||||
 | 
					  succeededRecords: RaRecord[];
 | 
				
			||||||
 | 
					  totalRecordCount: number;
 | 
				
			||||||
 | 
					  changeStats: ChangeStats;
 | 
				
			||||||
 | 
					  wasDryRun: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const FilePicker = () => {
 | 
					const FilePicker = () => {
 | 
				
			||||||
  const [values, setValues] = useState(null);
 | 
					  const [values, setValues] = useState<ImportLine[]>([]);
 | 
				
			||||||
  const [error, setError] = useState(null);
 | 
					  const [error, setError] = useState<string | string[] | null>(null);
 | 
				
			||||||
  const [stats, setStats] = useState(null);
 | 
					  const [stats, setStats] = useState<ChangeStats | null>(null);
 | 
				
			||||||
  const [dryRun, setDryRun] = useState(true);
 | 
					  const [dryRun, setDryRun] = useState(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [progress, setProgress] = useState(null);
 | 
					  const [progress, setProgress] = useState<Progress>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [importResults, setImportResults] = useState(null);
 | 
					  const [importResults, setImportResults] = useState<ImportResult | null>(null);
 | 
				
			||||||
  const [skippedRecords, setSkippedRecords] = useState(null);
 | 
					  const [skippedRecords, setSkippedRecords] = useState<string>("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [conflictMode, setConflictMode] = useState("stop");
 | 
					  const [conflictMode, setConflictMode] = useState("stop");
 | 
				
			||||||
  const [passwordMode, setPasswordMode] = useState(true);
 | 
					  const [passwordMode, setPasswordMode] = useState(true);
 | 
				
			||||||
| 
						 | 
					@ -52,14 +81,15 @@ const FilePicker = () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const dataProvider = useDataProvider();
 | 
					  const dataProvider = useDataProvider();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onFileChange = async e => {
 | 
					  const onFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
 | 
				
			||||||
    if (progress !== null) return;
 | 
					    if (progress !== null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setValues(null);
 | 
					    setValues([]);
 | 
				
			||||||
    setError(null);
 | 
					    setError(null);
 | 
				
			||||||
    setStats(null);
 | 
					    setStats(null);
 | 
				
			||||||
    setImportResults(null);
 | 
					    setImportResults(null);
 | 
				
			||||||
    const file = e.target.files ? e.target.files[0] : null;
 | 
					    const file = e.target.files ? e.target.files[0] : null;
 | 
				
			||||||
 | 
					    if (!file) return;
 | 
				
			||||||
    /* Let's refuse some unreasonably big files instead of freezing
 | 
					    /* Let's refuse some unreasonably big files instead of freezing
 | 
				
			||||||
     * up the browser */
 | 
					     * up the browser */
 | 
				
			||||||
    if (file.size > 100000000) {
 | 
					    if (file.size > 100000000) {
 | 
				
			||||||
| 
						 | 
					@ -71,12 +101,12 @@ const FilePicker = () => {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      parseCsv(file, {
 | 
					      parseCsv<ImportLine>(file, {
 | 
				
			||||||
        header: true,
 | 
					        header: true,
 | 
				
			||||||
        skipEmptyLines: true /* especially for a final EOL in the csv file */,
 | 
					        skipEmptyLines: true /* especially for a final EOL in the csv file */,
 | 
				
			||||||
        complete: result => {
 | 
					        complete: result => {
 | 
				
			||||||
          if (result.error) {
 | 
					          if (result.errors) {
 | 
				
			||||||
            setError(result.error);
 | 
					            setError(result.errors.map(e => e.toString()));
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          /* Papaparse is very lenient, we may be able to salvage
 | 
					          /* Papaparse is very lenient, we may be able to salvage
 | 
				
			||||||
           * the data in the file. */
 | 
					           * the data in the file. */
 | 
				
			||||||
| 
						 | 
					@ -84,32 +114,17 @@ const FilePicker = () => {
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    } catch {
 | 
					    } catch {
 | 
				
			||||||
      setError(true);
 | 
					      setError("Unknown error");
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const verifyCsv = (
 | 
					  const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
 | 
				
			||||||
    { data, meta, errors },
 | 
					 | 
				
			||||||
    { setValues, setStats, setError }
 | 
					 | 
				
			||||||
  ) => {
 | 
					 | 
				
			||||||
    /* First, verify the presence of required fields */
 | 
					    /* First, verify the presence of required fields */
 | 
				
			||||||
    let eF = Array.from(expectedFields);
 | 
					    const missingFields = expectedFields.filter(eF => meta.fields?.find(mF => eF === mF));
 | 
				
			||||||
    let oF = Array.from(optionalFields);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    meta.fields.forEach(name => {
 | 
					    if (missingFields.length > 0) {
 | 
				
			||||||
      if (eF.includes(name)) {
 | 
					      setError(translate("import_users.error.required_field", { field: missingFields[0] }));
 | 
				
			||||||
        eF = eF.filter(v => v !== name);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (oF.includes(name)) {
 | 
					 | 
				
			||||||
        oF = oF.filter(v => v !== name);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (eF.length !== 0) {
 | 
					 | 
				
			||||||
      setError(
 | 
					 | 
				
			||||||
        translate("import_users.error.required_field", { field: eF[0] })
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -119,7 +134,7 @@ const FilePicker = () => {
 | 
				
			||||||
    /* Collect some stats to prevent sneaky csv files from adding admin
 | 
					    /* Collect some stats to prevent sneaky csv files from adding admin
 | 
				
			||||||
       users or something.
 | 
					       users or something.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    let stats = {
 | 
					    const stats = {
 | 
				
			||||||
      user_types: { default: 0 },
 | 
					      user_types: { default: 0 },
 | 
				
			||||||
      is_guest: 0,
 | 
					      is_guest: 0,
 | 
				
			||||||
      admin: 0,
 | 
					      admin: 0,
 | 
				
			||||||
| 
						 | 
					@ -131,6 +146,7 @@ const FilePicker = () => {
 | 
				
			||||||
      total: data.length,
 | 
					      total: data.length,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const errorMessages = errors.map(e => e.message);
 | 
				
			||||||
    data.forEach((line, idx) => {
 | 
					    data.forEach((line, idx) => {
 | 
				
			||||||
      if (line.user_type === undefined || line.user_type === "") {
 | 
					      if (line.user_type === undefined || line.user_type === "") {
 | 
				
			||||||
        stats.user_types.default++;
 | 
					        stats.user_types.default++;
 | 
				
			||||||
| 
						 | 
					@ -141,14 +157,13 @@ const FilePicker = () => {
 | 
				
			||||||
       * resource so it gives sensible field names and doesn't duplicate
 | 
					       * resource so it gives sensible field names and doesn't duplicate
 | 
				
			||||||
       * id as "name"?
 | 
					       * id as "name"?
 | 
				
			||||||
       */
 | 
					       */
 | 
				
			||||||
      if (meta.fields.includes("name")) {
 | 
					      if (meta.fields?.includes("name")) {
 | 
				
			||||||
        delete line.name;
 | 
					        delete line.name;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (meta.fields.includes("user_type")) {
 | 
					      if (meta.fields?.includes("user_type")) {
 | 
				
			||||||
        delete line.user_type;
 | 
					        delete line.user_type;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (meta.fields.includes("is_admin")) {
 | 
					      if (meta.fields?.includes("is_admin")) {
 | 
				
			||||||
        line.admin = line.is_admin;
 | 
					 | 
				
			||||||
        delete line.is_admin;
 | 
					        delete line.is_admin;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -158,7 +173,7 @@ const FilePicker = () => {
 | 
				
			||||||
          line[f] = true; // we need true booleans instead of strings
 | 
					          line[f] = true; // we need true booleans instead of strings
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          if (line[f] !== "false" && line[f] !== "") {
 | 
					          if (line[f] !== "false" && line[f] !== "") {
 | 
				
			||||||
            errors.push(
 | 
					            errorMessages.push(
 | 
				
			||||||
              translate("import_users.error.invalid_value", {
 | 
					              translate("import_users.error.invalid_value", {
 | 
				
			||||||
                field: f,
 | 
					                field: f,
 | 
				
			||||||
                row: idx,
 | 
					                row: idx,
 | 
				
			||||||
| 
						 | 
					@ -182,8 +197,8 @@ const FilePicker = () => {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (errors.length > 0) {
 | 
					    if (errorMessages.length > 0) {
 | 
				
			||||||
      setError(errors);
 | 
					      setError(errorMessages);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    setStats(stats);
 | 
					    setStats(stats);
 | 
				
			||||||
    setValues(data);
 | 
					    setValues(data);
 | 
				
			||||||
| 
						 | 
					@ -191,7 +206,7 @@ const FilePicker = () => {
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const runImport = async _e => {
 | 
					  const runImport = async () => {
 | 
				
			||||||
    if (progress !== null) {
 | 
					    if (progress !== null) {
 | 
				
			||||||
      notify("import_users.errors.already_in_progress");
 | 
					      notify("import_users.errors.already_in_progress");
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
| 
						 | 
					@ -220,61 +235,40 @@ const FilePicker = () => {
 | 
				
			||||||
  //     which doesn't look very good.
 | 
					  //     which doesn't look very good.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const doImport = async (
 | 
					  const doImport = async (
 | 
				
			||||||
    dataProvider,
 | 
					    dataProvider: DataProvider,
 | 
				
			||||||
    data,
 | 
					    data: ImportLine[],
 | 
				
			||||||
    conflictMode,
 | 
					    conflictMode: string,
 | 
				
			||||||
    passwordMode,
 | 
					    passwordMode: boolean,
 | 
				
			||||||
    useridMode,
 | 
					    useridMode: string,
 | 
				
			||||||
    dryRun,
 | 
					    dryRun: boolean,
 | 
				
			||||||
    setProgress,
 | 
					    setProgress: (progress: Progress) => void,
 | 
				
			||||||
    setError
 | 
					    setError: (message: string) => void
 | 
				
			||||||
  ) => {
 | 
					  ): Promise<ImportResult> => {
 | 
				
			||||||
    let skippedRecords = [];
 | 
					    const skippedRecords: ImportLine[] = [];
 | 
				
			||||||
    let erroredRecords = [];
 | 
					    const erroredRecords: ImportLine[] = [];
 | 
				
			||||||
    let succeededRecords = [];
 | 
					    const succeededRecords: ImportLine[] = [];
 | 
				
			||||||
    let changeStats = {
 | 
					    const changeStats: ChangeStats = {
 | 
				
			||||||
      toAdmin: 0,
 | 
					      total: 0,
 | 
				
			||||||
      toGuest: 0,
 | 
					      id: 0,
 | 
				
			||||||
      toRegular: 0,
 | 
					      is_guest: 0,
 | 
				
			||||||
      replacedPassword: 0,
 | 
					      admin: 0,
 | 
				
			||||||
 | 
					      password: 0,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    let entriesDone = 0;
 | 
					    let entriesDone = 0;
 | 
				
			||||||
    let entriesCount = data.length;
 | 
					    const entriesCount = data.length;
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      setProgress({ done: entriesDone, limit: entriesCount });
 | 
					      setProgress({ done: entriesDone, limit: entriesCount });
 | 
				
			||||||
      for (const entry of data) {
 | 
					      for (const entry of data) {
 | 
				
			||||||
        let userRecord = {};
 | 
					        const userRecord = { ...entry };
 | 
				
			||||||
        let overwriteData = {};
 | 
					 | 
				
			||||||
        // No need to do a bunch of cryptographic random number getting if
 | 
					        // No need to do a bunch of cryptographic random number getting if
 | 
				
			||||||
        // we are using neither a generated password nor a generated user id.
 | 
					        // we are using neither a generated password nor a generated user id.
 | 
				
			||||||
        if (
 | 
					        if (useridMode === "ignore" || userRecord.id === undefined) {
 | 
				
			||||||
          useridMode === "ignore" ||
 | 
					          userRecord.id = generateRandomMxId();
 | 
				
			||||||
          entry.id === undefined ||
 | 
					        }
 | 
				
			||||||
          entry.password === undefined ||
 | 
					        if (passwordMode === false || entry.password === undefined) {
 | 
				
			||||||
          passwordMode === false
 | 
					          userRecord.password = generateRandomPassword();
 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
          overwriteData = generateRandomUser();
 | 
					 | 
				
			||||||
          // Ignoring IDs or the entry lacking an ID means we keep the
 | 
					 | 
				
			||||||
          // ID field in the overwrite data.
 | 
					 | 
				
			||||||
          if (!(useridMode === "ignore" || entry.id === undefined)) {
 | 
					 | 
				
			||||||
            delete overwriteData.id;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          // Not using passwords from the csv or this entry lacking a password
 | 
					 | 
				
			||||||
          // means we keep the password field in the overwrite data.
 | 
					 | 
				
			||||||
          if (
 | 
					 | 
				
			||||||
            !(
 | 
					 | 
				
			||||||
              passwordMode === false ||
 | 
					 | 
				
			||||||
              entry.password === undefined ||
 | 
					 | 
				
			||||||
              entry.password === ""
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
          ) {
 | 
					 | 
				
			||||||
            delete overwriteData.password;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        /* TODO record update stats (especially admin no -> yes, deactivated x -> !x, ... */
 | 
					        /* TODO record update stats (especially admin no -> yes, deactivated x -> !x, ... */
 | 
				
			||||||
        Object.assign(userRecord, entry);
 | 
					 | 
				
			||||||
        Object.assign(userRecord, overwriteData);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /* For these modes we will consider the ID that's in the record.
 | 
					        /* For these modes we will consider the ID that's in the record.
 | 
				
			||||||
         * If the mode is "stop", we will not continue adding more records, and
 | 
					         * If the mode is "stop", we will not continue adding more records, and
 | 
				
			||||||
| 
						 | 
					@ -300,14 +294,11 @@ const FilePicker = () => {
 | 
				
			||||||
         * We do a simple retry loop so that an accidental hit on an existing ID
 | 
					         * We do a simple retry loop so that an accidental hit on an existing ID
 | 
				
			||||||
         * doesn't trip us up.
 | 
					         * doesn't trip us up.
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        if (LOGGING)
 | 
					        if (LOGGING) console.log("will check for existence of record " + JSON.stringify(userRecord));
 | 
				
			||||||
          console.log(
 | 
					 | 
				
			||||||
            "will check for existence of record " + JSON.stringify(userRecord)
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        let retries = 0;
 | 
					        let retries = 0;
 | 
				
			||||||
        const submitRecord = recordData => {
 | 
					        const submitRecord = (recordData: ImportLine) => {
 | 
				
			||||||
          return dataProvider.getOne("users", { id: recordData.id }).then(
 | 
					          return dataProvider.getOne("users", { id: recordData.id }).then(
 | 
				
			||||||
            async _alreadyExists => {
 | 
					            async () => {
 | 
				
			||||||
              if (LOGGING) console.log("already existed");
 | 
					              if (LOGGING) console.log("already existed");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              if (useridMode === "update" || conflictMode === "skip") {
 | 
					              if (useridMode === "update" || conflictMode === "skip") {
 | 
				
			||||||
| 
						 | 
					@ -319,9 +310,8 @@ const FilePicker = () => {
 | 
				
			||||||
                  })
 | 
					                  })
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
              } else {
 | 
					              } else {
 | 
				
			||||||
                const overwriteData = generateRandomUser();
 | 
					 | 
				
			||||||
                const newRecordData = Object.assign({}, recordData, {
 | 
					                const newRecordData = Object.assign({}, recordData, {
 | 
				
			||||||
                  id: overwriteData.id,
 | 
					                  id: generateRandomMxId(),
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
                retries++;
 | 
					                retries++;
 | 
				
			||||||
                if (retries > 512) {
 | 
					                if (retries > 512) {
 | 
				
			||||||
| 
						 | 
					@ -332,15 +322,8 @@ const FilePicker = () => {
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            async _okToSubmit => {
 | 
					            async () => {
 | 
				
			||||||
              if (LOGGING)
 | 
					              if (LOGGING) console.log("OK to create record " + recordData.id + " (" + recordData.displayname + ").");
 | 
				
			||||||
                console.log(
 | 
					 | 
				
			||||||
                  "OK to create record " +
 | 
					 | 
				
			||||||
                    recordData.id +
 | 
					 | 
				
			||||||
                    " (" +
 | 
					 | 
				
			||||||
                    recordData.displayname +
 | 
					 | 
				
			||||||
                    ")."
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
              if (!dryRun) {
 | 
					              if (!dryRun) {
 | 
				
			||||||
                await dataProvider.create("users", { data: recordData });
 | 
					                await dataProvider.create("users", { data: recordData });
 | 
				
			||||||
| 
						 | 
					@ -360,7 +343,7 @@ const FilePicker = () => {
 | 
				
			||||||
      setError(
 | 
					      setError(
 | 
				
			||||||
        translate("import_users.error.at_entry", {
 | 
					        translate("import_users.error.at_entry", {
 | 
				
			||||||
          entry: entriesDone + 1,
 | 
					          entry: entriesDone + 1,
 | 
				
			||||||
          message: e.message,
 | 
					          message: e instanceof Error ? e.message : String(e),
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      setProgress(null);
 | 
					      setProgress(null);
 | 
				
			||||||
| 
						 | 
					@ -387,7 +370,7 @@ const FilePicker = () => {
 | 
				
			||||||
    element.click();
 | 
					    element.click();
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onConflictModeChanged = async e => {
 | 
					  const onConflictModeChanged = async (e: ChangeEvent<HTMLSelectElement>) => {
 | 
				
			||||||
    if (progress !== null) {
 | 
					    if (progress !== null) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -396,7 +379,7 @@ const FilePicker = () => {
 | 
				
			||||||
    setConflictMode(value);
 | 
					    setConflictMode(value);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onPasswordModeChange = e => {
 | 
					  const onPasswordModeChange = (e: ChangeEvent<HTMLInputElement>) => {
 | 
				
			||||||
    if (progress !== null) {
 | 
					    if (progress !== null) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -404,7 +387,7 @@ const FilePicker = () => {
 | 
				
			||||||
    setPasswordMode(e.target.checked);
 | 
					    setPasswordMode(e.target.checked);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onUseridModeChanged = async e => {
 | 
					  const onUseridModeChanged = async (e: ChangeEvent<HTMLSelectElement>) => {
 | 
				
			||||||
    if (progress !== null) {
 | 
					    if (progress !== null) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -413,11 +396,11 @@ const FilePicker = () => {
 | 
				
			||||||
    setUseridMode(value);
 | 
					    setUseridMode(value);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onDryRunModeChanged = ev => {
 | 
					  const onDryRunModeChanged = (e: ChangeEvent<HTMLInputElement>) => {
 | 
				
			||||||
    if (progress !== null) {
 | 
					    if (progress !== null) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    setDryRun(ev.target.checked);
 | 
					    setDryRun(e.target.checked);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // render individual small components
 | 
					  // render individual small components
 | 
				
			||||||
| 
						 | 
					@ -425,28 +408,11 @@ const FilePicker = () => {
 | 
				
			||||||
  const statsCards = stats &&
 | 
					  const statsCards = stats &&
 | 
				
			||||||
    !importResults && [
 | 
					    !importResults && [
 | 
				
			||||||
      <Container>
 | 
					      <Container>
 | 
				
			||||||
        <CardHeader
 | 
					        <CardHeader title={translate("import_users.cards.importstats.header")} />
 | 
				
			||||||
          title={translate("import_users.cards.importstats.header")}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <CardContent>
 | 
					        <CardContent>
 | 
				
			||||||
          <div>
 | 
					          <div>{translate("import_users.cards.importstats.users_total", stats.total)}</div>
 | 
				
			||||||
            {translate(
 | 
					          <div>{translate("import_users.cards.importstats.guest_count", stats.is_guest)}</div>
 | 
				
			||||||
              "import_users.cards.importstats.users_total",
 | 
					          <div>{translate("import_users.cards.importstats.admin_count", stats.admin)}</div>
 | 
				
			||||||
              stats.total
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <div>
 | 
					 | 
				
			||||||
            {translate(
 | 
					 | 
				
			||||||
              "import_users.cards.importstats.guest_count",
 | 
					 | 
				
			||||||
              stats.is_guest
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <div>
 | 
					 | 
				
			||||||
            {translate(
 | 
					 | 
				
			||||||
              "import_users.cards.importstats.admin_count",
 | 
					 | 
				
			||||||
              stats.admin
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </CardContent>
 | 
					        </CardContent>
 | 
				
			||||||
      </Container>,
 | 
					      </Container>,
 | 
				
			||||||
      <Container>
 | 
					      <Container>
 | 
				
			||||||
| 
						 | 
					@ -459,19 +425,9 @@ const FilePicker = () => {
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          {stats.id > 0 ? (
 | 
					          {stats.id > 0 ? (
 | 
				
			||||||
            <div>
 | 
					            <div>
 | 
				
			||||||
              <NativeSelect
 | 
					              <NativeSelect onChange={onUseridModeChanged} value={useridMode} disabled={progress !== null}>
 | 
				
			||||||
                onChange={onUseridModeChanged}
 | 
					                <TranslatableOption value="ignore" text="import_users.cards.ids.mode.ignore" />
 | 
				
			||||||
                value={useridMode}
 | 
					                <TranslatableOption value="update" text="import_users.cards.ids.mode.update" />
 | 
				
			||||||
                enabled={(progress !== null).toString()}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <TranslatableOption
 | 
					 | 
				
			||||||
                  value="ignore"
 | 
					 | 
				
			||||||
                  text="import_users.cards.ids.mode.ignore"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
                <TranslatableOption
 | 
					 | 
				
			||||||
                  value="update"
 | 
					 | 
				
			||||||
                  text="import_users.cards.ids.mode.update"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              </NativeSelect>
 | 
					              </NativeSelect>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          ) : (
 | 
					          ) : (
 | 
				
			||||||
| 
						 | 
					@ -485,20 +441,13 @@ const FilePicker = () => {
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
            {stats.password === stats.total
 | 
					            {stats.password === stats.total
 | 
				
			||||||
              ? translate("import_users.cards.passwords.all_passwords_present")
 | 
					              ? translate("import_users.cards.passwords.all_passwords_present")
 | 
				
			||||||
              : translate(
 | 
					              : translate("import_users.cards.passwords.count_passwords_present", stats.password)}
 | 
				
			||||||
                  "import_users.cards.passwords.count_passwords_present",
 | 
					 | 
				
			||||||
                  stats.password
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          {stats.password > 0 ? (
 | 
					          {stats.password > 0 ? (
 | 
				
			||||||
            <div>
 | 
					            <div>
 | 
				
			||||||
              <FormControlLabel
 | 
					              <FormControlLabel
 | 
				
			||||||
                control={
 | 
					                control={
 | 
				
			||||||
                  <Checkbox
 | 
					                  <Checkbox checked={passwordMode} disabled={progress !== null} onChange={onPasswordModeChange} />
 | 
				
			||||||
                    checked={passwordMode}
 | 
					 | 
				
			||||||
                    enabled={(progress !== null).toString()}
 | 
					 | 
				
			||||||
                    onChange={onPasswordModeChange}
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                label={translate("import_users.cards.passwords.use_passwords")}
 | 
					                label={translate("import_users.cards.passwords.use_passwords")}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
| 
						 | 
					@ -510,31 +459,21 @@ const FilePicker = () => {
 | 
				
			||||||
      </Container>,
 | 
					      </Container>,
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let conflictCards = stats && !importResults && (
 | 
					  const conflictCards = stats && !importResults && (
 | 
				
			||||||
    <Container>
 | 
					    <Container>
 | 
				
			||||||
      <CardHeader title={translate("import_users.cards.conflicts.header")} />
 | 
					      <CardHeader title={translate("import_users.cards.conflicts.header")} />
 | 
				
			||||||
      <CardContent>
 | 
					      <CardContent>
 | 
				
			||||||
        <div>
 | 
					        <div>
 | 
				
			||||||
          <NativeSelect
 | 
					          <NativeSelect onChange={onConflictModeChanged} value={conflictMode} disabled={progress !== null}>
 | 
				
			||||||
            onChange={onConflictModeChanged}
 | 
					            <TranslatableOption value="stop" text="import_users.cards.conflicts.mode.stop" />
 | 
				
			||||||
            value={conflictMode}
 | 
					            <TranslatableOption value="skip" text="import_users.cards.conflicts.mode.skip" />
 | 
				
			||||||
            enabled={(progress !== null).toString()}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <TranslatableOption
 | 
					 | 
				
			||||||
              value="stop"
 | 
					 | 
				
			||||||
              text="import_users.cards.conflicts.mode.stop"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <TranslatableOption
 | 
					 | 
				
			||||||
              value="skip"
 | 
					 | 
				
			||||||
              text="import_users.cards.conflicts.mode.skip"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </NativeSelect>
 | 
					          </NativeSelect>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </CardContent>
 | 
					      </CardContent>
 | 
				
			||||||
    </Container>
 | 
					    </Container>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let errorCards = error && (
 | 
					  const errorCards = error && (
 | 
				
			||||||
    <Container>
 | 
					    <Container>
 | 
				
			||||||
      <CardHeader title={translate("import_users.error.error")} />
 | 
					      <CardHeader title={translate("import_users.error.error")} />
 | 
				
			||||||
      <CardContent>
 | 
					      <CardContent>
 | 
				
			||||||
| 
						 | 
					@ -545,7 +484,7 @@ const FilePicker = () => {
 | 
				
			||||||
    </Container>
 | 
					    </Container>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let uploadCard = !importResults && (
 | 
					  const uploadCard = !importResults && (
 | 
				
			||||||
    <Container>
 | 
					    <Container>
 | 
				
			||||||
      <CardHeader title={translate("import_users.cards.upload.header")} />
 | 
					      <CardHeader title={translate("import_users.cards.upload.header")} />
 | 
				
			||||||
      <CardContent>
 | 
					      <CardContent>
 | 
				
			||||||
| 
						 | 
					@ -553,35 +492,22 @@ const FilePicker = () => {
 | 
				
			||||||
        <a href="./data/example.csv">example.csv</a>
 | 
					        <a href="./data/example.csv">example.csv</a>
 | 
				
			||||||
        <br />
 | 
					        <br />
 | 
				
			||||||
        <br />
 | 
					        <br />
 | 
				
			||||||
        <input
 | 
					        <input type="file" onChange={onFileChange} disabled={progress !== null} />
 | 
				
			||||||
          type="file"
 | 
					 | 
				
			||||||
          onChange={onFileChange}
 | 
					 | 
				
			||||||
          enabled={(progress !== null).toString()}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </CardContent>
 | 
					      </CardContent>
 | 
				
			||||||
    </Container>
 | 
					    </Container>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let resultsCard = importResults && (
 | 
					  const resultsCard = importResults && (
 | 
				
			||||||
    <CardContent>
 | 
					    <CardContent>
 | 
				
			||||||
      <CardHeader title={translate("import_users.cards.results.header")} />
 | 
					      <CardHeader title={translate("import_users.cards.results.header")} />
 | 
				
			||||||
      <div>
 | 
					      <div>
 | 
				
			||||||
        {translate(
 | 
					        {translate("import_users.cards.results.total", importResults.totalRecordCount)}
 | 
				
			||||||
          "import_users.cards.results.total",
 | 
					 | 
				
			||||||
          importResults.totalRecordCount
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
        <br />
 | 
					        <br />
 | 
				
			||||||
        {translate(
 | 
					        {translate("import_users.cards.results.successful", importResults.succeededRecords.length)}
 | 
				
			||||||
          "import_users.cards.results.successful",
 | 
					 | 
				
			||||||
          importResults.succeededRecords.length
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
        <br />
 | 
					        <br />
 | 
				
			||||||
        {importResults.skippedRecords.length
 | 
					        {importResults.skippedRecords.length
 | 
				
			||||||
          ? [
 | 
					          ? [
 | 
				
			||||||
              translate(
 | 
					              translate("import_users.cards.results.skipped", importResults.skippedRecords.length),
 | 
				
			||||||
                "import_users.cards.results.skipped",
 | 
					 | 
				
			||||||
                importResults.skippedRecords.length
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              <div>
 | 
					              <div>
 | 
				
			||||||
                <button onClick={downloadSkippedRecords}>
 | 
					                <button onClick={downloadSkippedRecords}>
 | 
				
			||||||
                  {translate("import_users.cards.results.download_skipped")}
 | 
					                  {translate("import_users.cards.results.download_skipped")}
 | 
				
			||||||
| 
						 | 
					@ -591,41 +517,22 @@ const FilePicker = () => {
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
          : ""}
 | 
					          : ""}
 | 
				
			||||||
        {importResults.erroredRecords.length
 | 
					        {importResults.erroredRecords.length
 | 
				
			||||||
          ? [
 | 
					          ? [translate("import_users.cards.results.skipped", importResults.erroredRecords.length), <br />]
 | 
				
			||||||
              translate(
 | 
					 | 
				
			||||||
                "import_users.cards.results.skipped",
 | 
					 | 
				
			||||||
                importResults.erroredRecords.length
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              <br />,
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
          : ""}
 | 
					          : ""}
 | 
				
			||||||
        <br />
 | 
					        <br />
 | 
				
			||||||
        {importResults.wasDryRun && [
 | 
					        {importResults.wasDryRun && [translate("import_users.cards.results.simulated_only"), <br />]}
 | 
				
			||||||
          translate("import_users.cards.results.simulated_only"),
 | 
					 | 
				
			||||||
          <br />,
 | 
					 | 
				
			||||||
        ]}
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </CardContent>
 | 
					    </CardContent>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let startImportCard =
 | 
					  const startImportCard =
 | 
				
			||||||
    !values || values.length === 0 || importResults ? undefined : (
 | 
					    !values || values.length === 0 || importResults ? undefined : (
 | 
				
			||||||
      <CardActions>
 | 
					      <CardActions>
 | 
				
			||||||
        <FormControlLabel
 | 
					        <FormControlLabel
 | 
				
			||||||
          control={
 | 
					          control={<Checkbox checked={dryRun} onChange={onDryRunModeChanged} disabled={progress !== null} />}
 | 
				
			||||||
            <Checkbox
 | 
					 | 
				
			||||||
              checked={dryRun}
 | 
					 | 
				
			||||||
              onChange={onDryRunModeChanged}
 | 
					 | 
				
			||||||
              enabled={(progress !== null).toString()}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          label={translate("import_users.cards.startImport.simulate_only")}
 | 
					          label={translate("import_users.cards.startImport.simulate_only")}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <Button
 | 
					        <Button size="large" onClick={runImport} disabled={progress !== null}>
 | 
				
			||||||
          size="large"
 | 
					 | 
				
			||||||
          onClick={runImport}
 | 
					 | 
				
			||||||
          enabled={(progress !== null).toString()}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          {translate("import_users.cards.startImport.run_import")}
 | 
					          {translate("import_users.cards.startImport.run_import")}
 | 
				
			||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
        {progress !== null ? (
 | 
					        {progress !== null ? (
 | 
				
			||||||
| 
						 | 
					@ -636,7 +543,7 @@ const FilePicker = () => {
 | 
				
			||||||
      </CardActions>
 | 
					      </CardActions>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let allCards = [];
 | 
					  const allCards: JSX.Element[] = [];
 | 
				
			||||||
  if (uploadCard) allCards.push(uploadCard);
 | 
					  if (uploadCard) allCards.push(uploadCard);
 | 
				
			||||||
  if (errorCards) allCards.push(errorCards);
 | 
					  if (errorCards) allCards.push(errorCards);
 | 
				
			||||||
  if (conflictCards) allCards.push(conflictCards);
 | 
					  if (conflictCards) allCards.push(conflictCards);
 | 
				
			||||||
| 
						 | 
					@ -644,12 +551,9 @@ const FilePicker = () => {
 | 
				
			||||||
  if (startImportCard) allCards.push(startImportCard);
 | 
					  if (startImportCard) allCards.push(startImportCard);
 | 
				
			||||||
  if (resultsCard) allCards.push(resultsCard);
 | 
					  if (resultsCard) allCards.push(resultsCard);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let cardContainer = <Card>{allCards}</Card>;
 | 
					  const cardContainer = <Card>{allCards}</Card>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return [
 | 
					  return [<Title defaultTitle={translate("import_users.title")} />, cardContainer];
 | 
				
			||||||
    <Title defaultTitle={translate("import_users.title")} />,
 | 
					 | 
				
			||||||
    cardContainer,
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ImportFeature = FilePicker;
 | 
					export const ImportFeature = FilePicker;
 | 
				
			||||||
| 
						 | 
					@ -1,71 +0,0 @@
 | 
				
			||||||
import React from "react";
 | 
					 | 
				
			||||||
import { render, screen } from "@testing-library/react";
 | 
					 | 
				
			||||||
import { AdminContext } from "react-admin";
 | 
					 | 
				
			||||||
import LoginPage from "./LoginPage";
 | 
					 | 
				
			||||||
import { AppContext } from "../AppContext";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe("LoginForm", () => {
 | 
					 | 
				
			||||||
  it("renders with no restriction to homeserver", () => {
 | 
					 | 
				
			||||||
    render(
 | 
					 | 
				
			||||||
      <AdminContext>
 | 
					 | 
				
			||||||
        <LoginPage />
 | 
					 | 
				
			||||||
      </AdminContext>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    screen.getByText("synapseadmin.auth.welcome");
 | 
					 | 
				
			||||||
    screen.getByRole("combobox", { name: "" });
 | 
					 | 
				
			||||||
    screen.getByRole("textbox", { name: "ra.auth.username" });
 | 
					 | 
				
			||||||
    screen.getByText("ra.auth.password");
 | 
					 | 
				
			||||||
    const baseUrlInput = screen.getByRole("textbox", {
 | 
					 | 
				
			||||||
      name: "synapseadmin.auth.base_url",
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    expect(baseUrlInput.className.split(" ")).not.toContain("Mui-readOnly");
 | 
					 | 
				
			||||||
    screen.getByRole("button", { name: "ra.auth.sign_in" });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  it("renders with single restricted homeserver", () => {
 | 
					 | 
				
			||||||
    render(
 | 
					 | 
				
			||||||
      <AppContext.Provider
 | 
					 | 
				
			||||||
        value={{ restrictBaseUrl: "https://matrix.example.com" }}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <AdminContext>
 | 
					 | 
				
			||||||
          <LoginPage />
 | 
					 | 
				
			||||||
        </AdminContext>
 | 
					 | 
				
			||||||
      </AppContext.Provider>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    screen.getByText("synapseadmin.auth.welcome");
 | 
					 | 
				
			||||||
    screen.getByRole("combobox", { name: "" });
 | 
					 | 
				
			||||||
    screen.getByRole("textbox", { name: "ra.auth.username" });
 | 
					 | 
				
			||||||
    screen.getByText("ra.auth.password");
 | 
					 | 
				
			||||||
    const baseUrlInput = screen.getByRole("textbox", {
 | 
					 | 
				
			||||||
      name: "synapseadmin.auth.base_url",
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    expect(baseUrlInput.className.split(" ")).toContain("Mui-readOnly");
 | 
					 | 
				
			||||||
    screen.getByRole("button", { name: "ra.auth.sign_in" });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  it("renders with multiple restricted homeservers", async () => {
 | 
					 | 
				
			||||||
    render(
 | 
					 | 
				
			||||||
      <AppContext.Provider
 | 
					 | 
				
			||||||
        value={{
 | 
					 | 
				
			||||||
          restrictBaseUrl: [
 | 
					 | 
				
			||||||
            "https://matrix.example.com",
 | 
					 | 
				
			||||||
            "https://matrix.example.org",
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <AdminContext>
 | 
					 | 
				
			||||||
          <LoginPage />
 | 
					 | 
				
			||||||
        </AdminContext>
 | 
					 | 
				
			||||||
      </AppContext.Provider>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    screen.getByText("synapseadmin.auth.welcome");
 | 
					 | 
				
			||||||
    screen.getByRole("combobox", { name: "" });
 | 
					 | 
				
			||||||
    screen.getByRole("textbox", { name: "ra.auth.username" });
 | 
					 | 
				
			||||||
    screen.getByText("ra.auth.password");
 | 
					 | 
				
			||||||
    screen.getByRole("combobox", { name: "synapseadmin.auth.base_url" });
 | 
					 | 
				
			||||||
    screen.getByRole("button", { name: "ra.auth.sign_in" });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
							
								
								
									
										73
									
								
								src/components/LoginPage.test.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/components/LoginPage.test.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,73 @@
 | 
				
			||||||
 | 
					import polyglotI18nProvider from "ra-i18n-polyglot";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { render, screen } from "@testing-library/react";
 | 
				
			||||||
 | 
					import { AdminContext } from "react-admin";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import LoginPage from "./LoginPage";
 | 
				
			||||||
 | 
					import { AppContext } from "../AppContext";
 | 
				
			||||||
 | 
					import englishMessages from "../i18n/en";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("LoginForm", () => {
 | 
				
			||||||
 | 
					  it("renders with no restriction to homeserver", () => {
 | 
				
			||||||
 | 
					    render(
 | 
				
			||||||
 | 
					      <AdminContext i18nProvider={i18nProvider}>
 | 
				
			||||||
 | 
					        <LoginPage />
 | 
				
			||||||
 | 
					      </AdminContext>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    screen.getByText(englishMessages.synapseadmin.auth.welcome);
 | 
				
			||||||
 | 
					    screen.getByRole("combobox", { name: "" });
 | 
				
			||||||
 | 
					    screen.getByRole("textbox", { name: englishMessages.ra.auth.username });
 | 
				
			||||||
 | 
					    screen.getByText(englishMessages.ra.auth.password);
 | 
				
			||||||
 | 
					    const baseUrlInput = screen.getByRole("textbox", {
 | 
				
			||||||
 | 
					      name: englishMessages.synapseadmin.auth.base_url,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    expect(baseUrlInput.className.split(" ")).not.toContain("Mui-readOnly");
 | 
				
			||||||
 | 
					    screen.getByRole("button", { name: englishMessages.ra.auth.sign_in });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it("renders with single restricted homeserver", () => {
 | 
				
			||||||
 | 
					    render(
 | 
				
			||||||
 | 
					      <AppContext.Provider value={{ restrictBaseUrl: "https://matrix.example.com" }}>
 | 
				
			||||||
 | 
					        <AdminContext i18nProvider={i18nProvider}>
 | 
				
			||||||
 | 
					          <LoginPage />
 | 
				
			||||||
 | 
					        </AdminContext>
 | 
				
			||||||
 | 
					      </AppContext.Provider>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    screen.getByText(englishMessages.synapseadmin.auth.welcome);
 | 
				
			||||||
 | 
					    screen.getByRole("combobox", { name: "" });
 | 
				
			||||||
 | 
					    screen.getByRole("textbox", { name: englishMessages.ra.auth.username });
 | 
				
			||||||
 | 
					    screen.getByText(englishMessages.ra.auth.password);
 | 
				
			||||||
 | 
					    const baseUrlInput = screen.getByRole("textbox", {
 | 
				
			||||||
 | 
					      name: englishMessages.synapseadmin.auth.base_url,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    expect(baseUrlInput.className.split(" ")).toContain("Mui-readOnly");
 | 
				
			||||||
 | 
					    screen.getByRole("button", { name: englishMessages.ra.auth.sign_in });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it("renders with multiple restricted homeservers", async () => {
 | 
				
			||||||
 | 
					    render(
 | 
				
			||||||
 | 
					      <AppContext.Provider
 | 
				
			||||||
 | 
					        value={{
 | 
				
			||||||
 | 
					          restrictBaseUrl: ["https://matrix.example.com", "https://matrix.example.org"],
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <AdminContext i18nProvider={i18nProvider}>
 | 
				
			||||||
 | 
					          <LoginPage />
 | 
				
			||||||
 | 
					        </AdminContext>
 | 
				
			||||||
 | 
					      </AppContext.Provider>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    screen.getByText(englishMessages.synapseadmin.auth.welcome);
 | 
				
			||||||
 | 
					    screen.getByRole("combobox", { name: "" });
 | 
				
			||||||
 | 
					    screen.getByRole("textbox", { name: englishMessages.ra.auth.username });
 | 
				
			||||||
 | 
					    screen.getByText(englishMessages.ra.auth.password);
 | 
				
			||||||
 | 
					    screen.getByRole("combobox", {
 | 
				
			||||||
 | 
					      name: englishMessages.synapseadmin.auth.base_url,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    screen.getByRole("button", { name: englishMessages.ra.auth.sign_in });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,8 @@
 | 
				
			||||||
import React, { useState, useEffect } from "react";
 | 
					import { useState, useEffect } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import LockIcon from "@mui/icons-material/Lock";
 | 
				
			||||||
 | 
					import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Typography } from "@mui/material";
 | 
				
			||||||
 | 
					import { styled } from "@mui/material/styles";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Form,
 | 
					  Form,
 | 
				
			||||||
  FormDataConsumer,
 | 
					  FormDataConsumer,
 | 
				
			||||||
| 
						 | 
					@ -13,19 +17,6 @@ import {
 | 
				
			||||||
  useLocales,
 | 
					  useLocales,
 | 
				
			||||||
} from "react-admin";
 | 
					} from "react-admin";
 | 
				
			||||||
import { useFormContext } from "react-hook-form";
 | 
					import { useFormContext } from "react-hook-form";
 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  Avatar,
 | 
					 | 
				
			||||||
  Box,
 | 
					 | 
				
			||||||
  Button,
 | 
					 | 
				
			||||||
  Card,
 | 
					 | 
				
			||||||
  CardActions,
 | 
					 | 
				
			||||||
  CircularProgress,
 | 
					 | 
				
			||||||
  MenuItem,
 | 
					 | 
				
			||||||
  Select,
 | 
					 | 
				
			||||||
  Typography,
 | 
					 | 
				
			||||||
} from "@mui/material";
 | 
					 | 
				
			||||||
import { styled } from "@mui/material/styles";
 | 
					 | 
				
			||||||
import LockIcon from "@mui/icons-material/Lock";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useAppContext } from "../AppContext";
 | 
					import { useAppContext } from "../AppContext";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -103,9 +94,7 @@ const LoginPage = () => {
 | 
				
			||||||
  const [locale, setLocale] = useLocaleState();
 | 
					  const [locale, setLocale] = useLocaleState();
 | 
				
			||||||
  const locales = useLocales();
 | 
					  const locales = useLocales();
 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
  const base_url = allowSingleBaseUrl
 | 
					  const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url");
 | 
				
			||||||
    ? 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 = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -113,11 +102,7 @@ const LoginPage = () => {
 | 
				
			||||||
    const ssoToken = loginToken[1];
 | 
					    const ssoToken = loginToken[1];
 | 
				
			||||||
    console.log("SSO token is", ssoToken);
 | 
					    console.log("SSO token is", ssoToken);
 | 
				
			||||||
    // Prevent further requests
 | 
					    // Prevent further requests
 | 
				
			||||||
    window.history.replaceState(
 | 
					    window.history.replaceState({}, "", window.location.href.replace(loginToken[0], "#").split("#")[0]);
 | 
				
			||||||
      {},
 | 
					 | 
				
			||||||
      "",
 | 
					 | 
				
			||||||
      window.location.href.replace(loginToken[0], "#").split("#")[0]
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    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) {
 | 
				
			||||||
| 
						 | 
					@ -146,9 +131,7 @@ const LoginPage = () => {
 | 
				
			||||||
  const validateBaseUrl = value => {
 | 
					  const validateBaseUrl = value => {
 | 
				
			||||||
    if (!value.match(/^(http|https):\/\//)) {
 | 
					    if (!value.match(/^(http|https):\/\//)) {
 | 
				
			||||||
      return translate("synapseadmin.auth.protocol_error");
 | 
					      return translate("synapseadmin.auth.protocol_error");
 | 
				
			||||||
    } else if (
 | 
					    } else if (!value.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/)) {
 | 
				
			||||||
      !value.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/)
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      return translate("synapseadmin.auth.url_error");
 | 
					      return translate("synapseadmin.auth.url_error");
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return undefined;
 | 
					      return undefined;
 | 
				
			||||||
| 
						 | 
					@ -183,16 +166,13 @@ const LoginPage = () => {
 | 
				
			||||||
    const [serverVersion, setServerVersion] = useState("");
 | 
					    const [serverVersion, setServerVersion] = useState("");
 | 
				
			||||||
    const [matrixVersions, setMatrixVersions] = useState("");
 | 
					    const [matrixVersions, setMatrixVersions] = useState("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleUsernameChange = _ => {
 | 
					    const handleUsernameChange = () => {
 | 
				
			||||||
      if (formData.base_url || allowSingleBaseUrl) return;
 | 
					      if (formData.base_url || allowSingleBaseUrl) return;
 | 
				
			||||||
      // check if username is a full qualified userId then set base_url accordingly
 | 
					      // check if username is a full qualified userId then set base_url accordingly
 | 
				
			||||||
      const domain = splitMxid(formData.username)?.domain;
 | 
					      const domain = splitMxid(formData.username)?.domain;
 | 
				
			||||||
      if (domain) {
 | 
					      if (domain) {
 | 
				
			||||||
        getWellKnownUrl(domain).then(url => {
 | 
					        getWellKnownUrl(domain).then(url => {
 | 
				
			||||||
          if (
 | 
					          if (allowAnyBaseUrl || (allowMultipleBaseUrls && restrictBaseUrl.includes(url)))
 | 
				
			||||||
            allowAnyBaseUrl ||
 | 
					 | 
				
			||||||
            (allowMultipleBaseUrls && restrictBaseUrl.includes(url))
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
            form.setValue("base_url", url);
 | 
					            form.setValue("base_url", url);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
| 
						 | 
					@ -205,28 +185,20 @@ const LoginPage = () => {
 | 
				
			||||||
      if (!isValidBaseUrl(formData.base_url)) return;
 | 
					      if (!isValidBaseUrl(formData.base_url)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      getServerVersion(formData.base_url)
 | 
					      getServerVersion(formData.base_url)
 | 
				
			||||||
        .then(serverVersion =>
 | 
					        .then(serverVersion => setServerVersion(`${translate("synapseadmin.auth.server_version")} ${serverVersion}`))
 | 
				
			||||||
          setServerVersion(
 | 
					 | 
				
			||||||
            `${translate("synapseadmin.auth.server_version")} ${serverVersion}`
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .catch(() => setServerVersion(""));
 | 
					        .catch(() => setServerVersion(""));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      getSupportedFeatures(formData.base_url)
 | 
					      getSupportedFeatures(formData.base_url)
 | 
				
			||||||
        .then(features =>
 | 
					        .then(features =>
 | 
				
			||||||
          setMatrixVersions(
 | 
					          setMatrixVersions(`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`)
 | 
				
			||||||
            `${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .catch(() => setMatrixVersions(""));
 | 
					        .catch(() => setMatrixVersions(""));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Set SSO Url
 | 
					      // Set SSO Url
 | 
				
			||||||
      getSupportedLoginFlows(formData.base_url)
 | 
					      getSupportedLoginFlows(formData.base_url)
 | 
				
			||||||
        .then(loginFlows => {
 | 
					        .then(loginFlows => {
 | 
				
			||||||
          const supportPass =
 | 
					          const supportPass = loginFlows.find(f => f.type === "m.login.password") !== undefined;
 | 
				
			||||||
            loginFlows.find(f => f.type === "m.login.password") !== undefined;
 | 
					          const supportSSO = loginFlows.find(f => f.type === "m.login.sso") !== undefined;
 | 
				
			||||||
          const supportSSO =
 | 
					 | 
				
			||||||
            loginFlows.find(f => f.type === "m.login.sso") !== undefined;
 | 
					 | 
				
			||||||
          setSupportPassAuth(supportPass);
 | 
					          setSupportPassAuth(supportPass);
 | 
				
			||||||
          setSSOBaseUrl(supportSSO ? formData.base_url : "");
 | 
					          setSSOBaseUrl(supportSSO ? formData.base_url : "");
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
| 
						 | 
					@ -238,7 +210,7 @@ const LoginPage = () => {
 | 
				
			||||||
        <Box>
 | 
					        <Box>
 | 
				
			||||||
          <TextInput
 | 
					          <TextInput
 | 
				
			||||||
            autoFocus
 | 
					            autoFocus
 | 
				
			||||||
            name="username"
 | 
					            source="username"
 | 
				
			||||||
            label="ra.auth.username"
 | 
					            label="ra.auth.username"
 | 
				
			||||||
            autoComplete="username"
 | 
					            autoComplete="username"
 | 
				
			||||||
            disabled={loading || !supportPassAuth}
 | 
					            disabled={loading || !supportPassAuth}
 | 
				
			||||||
| 
						 | 
					@ -250,7 +222,7 @@ const LoginPage = () => {
 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
        <Box>
 | 
					        <Box>
 | 
				
			||||||
          <PasswordInput
 | 
					          <PasswordInput
 | 
				
			||||||
            name="password"
 | 
					            source="password"
 | 
				
			||||||
            label="ra.auth.password"
 | 
					            label="ra.auth.password"
 | 
				
			||||||
            type="password"
 | 
					            type="password"
 | 
				
			||||||
            autoComplete="current-password"
 | 
					            autoComplete="current-password"
 | 
				
			||||||
| 
						 | 
					@ -262,7 +234,7 @@ const LoginPage = () => {
 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
        <Box>
 | 
					        <Box>
 | 
				
			||||||
          <TextInput
 | 
					          <TextInput
 | 
				
			||||||
            name="base_url"
 | 
					            source="base_url"
 | 
				
			||||||
            label="synapseadmin.auth.base_url"
 | 
					            label="synapseadmin.auth.base_url"
 | 
				
			||||||
            select={allowMultipleBaseUrls}
 | 
					            select={allowMultipleBaseUrls}
 | 
				
			||||||
            autoComplete="url"
 | 
					            autoComplete="url"
 | 
				
			||||||
| 
						 | 
					@ -287,11 +259,7 @@ const LoginPage = () => {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Form
 | 
					    <Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onTouched">
 | 
				
			||||||
      defaultValues={{ base_url: base_url }}
 | 
					 | 
				
			||||||
      onSubmit={handleSubmit}
 | 
					 | 
				
			||||||
      mode="onTouched"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <FormBox>
 | 
					      <FormBox>
 | 
				
			||||||
        <Card className="card">
 | 
					        <Card className="card">
 | 
				
			||||||
          <Box className="avatar">
 | 
					          <Box className="avatar">
 | 
				
			||||||
| 
						 | 
					@ -318,9 +286,7 @@ const LoginPage = () => {
 | 
				
			||||||
                </MenuItem>
 | 
					                </MenuItem>
 | 
				
			||||||
              ))}
 | 
					              ))}
 | 
				
			||||||
            </Select>
 | 
					            </Select>
 | 
				
			||||||
            <FormDataConsumer>
 | 
					            <FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer>
 | 
				
			||||||
              {formDataProps => <UserData {...formDataProps} />}
 | 
					 | 
				
			||||||
            </FormDataConsumer>
 | 
					 | 
				
			||||||
            <CardActions className="actions">
 | 
					            <CardActions className="actions">
 | 
				
			||||||
              <Button
 | 
					              <Button
 | 
				
			||||||
                variant="contained"
 | 
					                variant="contained"
 | 
				
			||||||
| 
						 | 
					@ -1,62 +1,37 @@
 | 
				
			||||||
import React from "react";
 | 
					import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  BooleanInput,
 | 
					  BooleanInput,
 | 
				
			||||||
  Create,
 | 
					  Create,
 | 
				
			||||||
 | 
					  CreateProps,
 | 
				
			||||||
  Datagrid,
 | 
					  Datagrid,
 | 
				
			||||||
  DateField,
 | 
					  DateField,
 | 
				
			||||||
  DateTimeInput,
 | 
					  DateTimeInput,
 | 
				
			||||||
  Edit,
 | 
					  Edit,
 | 
				
			||||||
 | 
					  EditProps,
 | 
				
			||||||
  List,
 | 
					  List,
 | 
				
			||||||
 | 
					  ListProps,
 | 
				
			||||||
  maxValue,
 | 
					  maxValue,
 | 
				
			||||||
  number,
 | 
					  number,
 | 
				
			||||||
  NumberField,
 | 
					  NumberField,
 | 
				
			||||||
  NumberInput,
 | 
					  NumberInput,
 | 
				
			||||||
  regex,
 | 
					  regex,
 | 
				
			||||||
 | 
					  ResourceProps,
 | 
				
			||||||
  SaveButton,
 | 
					  SaveButton,
 | 
				
			||||||
  SimpleForm,
 | 
					  SimpleForm,
 | 
				
			||||||
  TextInput,
 | 
					  TextInput,
 | 
				
			||||||
  TextField,
 | 
					  TextField,
 | 
				
			||||||
  Toolbar,
 | 
					  Toolbar,
 | 
				
			||||||
} from "react-admin";
 | 
					} from "react-admin";
 | 
				
			||||||
import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const date_format = {
 | 
					import { DATE_FORMAT, dateFormatter, dateParser } from "./date";
 | 
				
			||||||
  year: "numeric",
 | 
					 | 
				
			||||||
  month: "2-digit",
 | 
					 | 
				
			||||||
  day: "2-digit",
 | 
					 | 
				
			||||||
  hour: "2-digit",
 | 
					 | 
				
			||||||
  minute: "2-digit",
 | 
					 | 
				
			||||||
  second: "2-digit",
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
 | 
					const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
 | 
				
			||||||
const validateUsesAllowed = [number()];
 | 
					const validateUsesAllowed = [number()];
 | 
				
			||||||
const validateLength = [number(), maxValue(64)];
 | 
					const validateLength = [number(), maxValue(64)];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const dateParser = v => {
 | 
					 | 
				
			||||||
  const d = new Date(v);
 | 
					 | 
				
			||||||
  if (isNaN(d)) return 0;
 | 
					 | 
				
			||||||
  return d.getTime();
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const dateFormatter = v => {
 | 
					 | 
				
			||||||
  if (v === undefined || v === null) return;
 | 
					 | 
				
			||||||
  const d = new Date(v);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const pad = "00";
 | 
					 | 
				
			||||||
  const year = d.getFullYear().toString();
 | 
					 | 
				
			||||||
  const month = (pad + (d.getMonth() + 1).toString()).slice(-2);
 | 
					 | 
				
			||||||
  const day = (pad + d.getDate().toString()).slice(-2);
 | 
					 | 
				
			||||||
  const hour = (pad + d.getHours().toString()).slice(-2);
 | 
					 | 
				
			||||||
  const minute = (pad + d.getMinutes().toString()).slice(-2);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // target format yyyy-MM-ddThh:mm
 | 
					 | 
				
			||||||
  return `${year}-${month}-${day}T${hour}:${minute}`;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />];
 | 
					const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RegistrationTokenList = props => (
 | 
					export const RegistrationTokenList = (props: ListProps) => (
 | 
				
			||||||
  <List
 | 
					  <List
 | 
				
			||||||
    {...props}
 | 
					    {...props}
 | 
				
			||||||
    filters={registrationTokenFilters}
 | 
					    filters={registrationTokenFilters}
 | 
				
			||||||
| 
						 | 
					@ -69,17 +44,12 @@ export const RegistrationTokenList = props => (
 | 
				
			||||||
      <NumberField source="uses_allowed" sortable={false} />
 | 
					      <NumberField source="uses_allowed" sortable={false} />
 | 
				
			||||||
      <NumberField source="pending" sortable={false} />
 | 
					      <NumberField source="pending" sortable={false} />
 | 
				
			||||||
      <NumberField source="completed" sortable={false} />
 | 
					      <NumberField source="completed" sortable={false} />
 | 
				
			||||||
      <DateField
 | 
					      <DateField source="expiry_time" showTime options={DATE_FORMAT} sortable={false} />
 | 
				
			||||||
        source="expiry_time"
 | 
					 | 
				
			||||||
        showTime
 | 
					 | 
				
			||||||
        options={date_format}
 | 
					 | 
				
			||||||
        sortable={false}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </Datagrid>
 | 
					    </Datagrid>
 | 
				
			||||||
  </List>
 | 
					  </List>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RegistrationTokenCreate = props => (
 | 
					export const RegistrationTokenCreate = (props: CreateProps) => (
 | 
				
			||||||
  <Create {...props} redirect="list">
 | 
					  <Create {...props} redirect="list">
 | 
				
			||||||
    <SimpleForm
 | 
					    <SimpleForm
 | 
				
			||||||
      toolbar={
 | 
					      toolbar={
 | 
				
			||||||
| 
						 | 
					@ -89,49 +59,32 @@ export const RegistrationTokenCreate = props => (
 | 
				
			||||||
        </Toolbar>
 | 
					        </Toolbar>
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <TextInput
 | 
					      <TextInput source="token" autoComplete="off" validate={validateToken} resettable />
 | 
				
			||||||
        source="token"
 | 
					 | 
				
			||||||
        autoComplete="off"
 | 
					 | 
				
			||||||
        validate={validateToken}
 | 
					 | 
				
			||||||
        resettable
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <NumberInput
 | 
					      <NumberInput
 | 
				
			||||||
        source="length"
 | 
					        source="length"
 | 
				
			||||||
        validate={validateLength}
 | 
					        validate={validateLength}
 | 
				
			||||||
        helperText="resources.registration_tokens.helper.length"
 | 
					        helperText="resources.registration_tokens.helper.length"
 | 
				
			||||||
        step={1}
 | 
					        step={1}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <NumberInput
 | 
					      <NumberInput source="uses_allowed" validate={validateUsesAllowed} step={1} />
 | 
				
			||||||
        source="uses_allowed"
 | 
					 | 
				
			||||||
        validate={validateUsesAllowed}
 | 
					 | 
				
			||||||
        step={1}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <DateTimeInput source="expiry_time" parse={dateParser} />
 | 
					      <DateTimeInput source="expiry_time" parse={dateParser} />
 | 
				
			||||||
    </SimpleForm>
 | 
					    </SimpleForm>
 | 
				
			||||||
  </Create>
 | 
					  </Create>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RegistrationTokenEdit = props => (
 | 
					export const RegistrationTokenEdit = (props: EditProps) => (
 | 
				
			||||||
  <Edit {...props}>
 | 
					  <Edit {...props}>
 | 
				
			||||||
    <SimpleForm>
 | 
					    <SimpleForm>
 | 
				
			||||||
      <TextInput source="token" disabled />
 | 
					      <TextInput source="token" disabled />
 | 
				
			||||||
      <NumberInput source="pending" disabled />
 | 
					      <NumberInput source="pending" disabled />
 | 
				
			||||||
      <NumberInput source="completed" disabled />
 | 
					      <NumberInput source="completed" disabled />
 | 
				
			||||||
      <NumberInput
 | 
					      <NumberInput source="uses_allowed" validate={validateUsesAllowed} step={1} />
 | 
				
			||||||
        source="uses_allowed"
 | 
					      <DateTimeInput source="expiry_time" parse={dateParser} format={dateFormatter} />
 | 
				
			||||||
        validate={validateUsesAllowed}
 | 
					 | 
				
			||||||
        step={1}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <DateTimeInput
 | 
					 | 
				
			||||||
        source="expiry_time"
 | 
					 | 
				
			||||||
        parse={dateParser}
 | 
					 | 
				
			||||||
        format={dateFormatter}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </SimpleForm>
 | 
					    </SimpleForm>
 | 
				
			||||||
  </Edit>
 | 
					  </Edit>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const resource = {
 | 
					const resource: ResourceProps = {
 | 
				
			||||||
  name: "registration_tokens",
 | 
					  name: "registration_tokens",
 | 
				
			||||||
  icon: RegistrationTokenIcon,
 | 
					  icon: RegistrationTokenIcon,
 | 
				
			||||||
  list: RegistrationTokenList,
 | 
					  list: RegistrationTokenList,
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,18 @@
 | 
				
			||||||
import React from "react";
 | 
					import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  BooleanField,
 | 
					  BooleanField,
 | 
				
			||||||
  BulkDeleteButton,
 | 
					  BulkDeleteButton,
 | 
				
			||||||
 | 
					  BulkDeleteButtonProps,
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
 | 
					  ButtonProps,
 | 
				
			||||||
  DatagridConfigurable,
 | 
					  DatagridConfigurable,
 | 
				
			||||||
 | 
					  DeleteButtonProps,
 | 
				
			||||||
  ExportButton,
 | 
					  ExportButton,
 | 
				
			||||||
  DeleteButton,
 | 
					  DeleteButton,
 | 
				
			||||||
  List,
 | 
					  List,
 | 
				
			||||||
  NumberField,
 | 
					  NumberField,
 | 
				
			||||||
  Pagination,
 | 
					  Pagination,
 | 
				
			||||||
 | 
					  ResourceProps,
 | 
				
			||||||
  SelectColumnsButton,
 | 
					  SelectColumnsButton,
 | 
				
			||||||
  TextField,
 | 
					  TextField,
 | 
				
			||||||
  TopToolbar,
 | 
					  TopToolbar,
 | 
				
			||||||
| 
						 | 
					@ -22,14 +26,12 @@ import {
 | 
				
			||||||
  useUnselectAll,
 | 
					  useUnselectAll,
 | 
				
			||||||
} from "react-admin";
 | 
					} from "react-admin";
 | 
				
			||||||
import { useMutation } from "react-query";
 | 
					import { useMutation } from "react-query";
 | 
				
			||||||
import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
 | 
					
 | 
				
			||||||
import AvatarField from "./AvatarField";
 | 
					import AvatarField from "./AvatarField";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const RoomDirectoryPagination = () => (
 | 
					const RoomDirectoryPagination = () => <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />;
 | 
				
			||||||
  <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RoomDirectoryUnpublishButton = props => {
 | 
					export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => {
 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
| 
						 | 
					@ -50,7 +52,7 @@ export const RoomDirectoryUnpublishButton = props => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RoomDirectoryBulkUnpublishButton = props => (
 | 
					export const RoomDirectoryBulkUnpublishButton = (props: BulkDeleteButtonProps) => (
 | 
				
			||||||
  <BulkDeleteButton
 | 
					  <BulkDeleteButton
 | 
				
			||||||
    {...props}
 | 
					    {...props}
 | 
				
			||||||
    label="resources.room_directory.action.erase"
 | 
					    label="resources.room_directory.action.erase"
 | 
				
			||||||
| 
						 | 
					@ -62,7 +64,7 @@ export const RoomDirectoryBulkUnpublishButton = props => (
 | 
				
			||||||
  />
 | 
					  />
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RoomDirectoryBulkPublishButton = props => {
 | 
					export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
 | 
				
			||||||
  const { selectedIds } = useListContext();
 | 
					  const { selectedIds } = useListContext();
 | 
				
			||||||
  const notify = useNotify();
 | 
					  const notify = useNotify();
 | 
				
			||||||
  const refresh = useRefresh();
 | 
					  const refresh = useRefresh();
 | 
				
			||||||
| 
						 | 
					@ -88,18 +90,13 @@ export const RoomDirectoryBulkPublishButton = props => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Button
 | 
					    <Button {...props} label="resources.room_directory.action.create" onClick={mutate} disabled={isLoading}>
 | 
				
			||||||
      {...props}
 | 
					 | 
				
			||||||
      label="resources.room_directory.action.create"
 | 
					 | 
				
			||||||
      onClick={mutate}
 | 
					 | 
				
			||||||
      disabled={isLoading}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <RoomDirectoryIcon />
 | 
					      <RoomDirectoryIcon />
 | 
				
			||||||
    </Button>
 | 
					    </Button>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RoomDirectoryPublishButton = props => {
 | 
					export const RoomDirectoryPublishButton = (props: ButtonProps) => {
 | 
				
			||||||
  const record = useRecordContext();
 | 
					  const record = useRecordContext();
 | 
				
			||||||
  const notify = useNotify();
 | 
					  const notify = useNotify();
 | 
				
			||||||
  const refresh = useRefresh();
 | 
					  const refresh = useRefresh();
 | 
				
			||||||
| 
						 | 
					@ -123,12 +120,7 @@ export const RoomDirectoryPublishButton = props => {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Button
 | 
					    <Button {...props} label="resources.room_directory.action.create" onClick={handleSend} disabled={isLoading}>
 | 
				
			||||||
      {...props}
 | 
					 | 
				
			||||||
      label="resources.room_directory.action.create"
 | 
					 | 
				
			||||||
      onClick={handleSend}
 | 
					 | 
				
			||||||
      disabled={isLoading}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <RoomDirectoryIcon />
 | 
					      <RoomDirectoryIcon />
 | 
				
			||||||
    </Button>
 | 
					    </Button>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
| 
						 | 
					@ -142,13 +134,9 @@ const RoomDirectoryListActions = () => (
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RoomDirectoryList = () => (
 | 
					export const RoomDirectoryList = () => (
 | 
				
			||||||
  <List
 | 
					  <List pagination={<RoomDirectoryPagination />} perPage={100} actions={<RoomDirectoryListActions />}>
 | 
				
			||||||
    pagination={<RoomDirectoryPagination />}
 | 
					 | 
				
			||||||
    perPage={100}
 | 
					 | 
				
			||||||
    actions={<RoomDirectoryListActions />}
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <DatagridConfigurable
 | 
					    <DatagridConfigurable
 | 
				
			||||||
      rowClick={(id, _resource, _record) => "/rooms/" + id + "/show"}
 | 
					      rowClick={id => "/rooms/" + id + "/show"}
 | 
				
			||||||
      bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
 | 
					      bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
 | 
				
			||||||
      omit={["room_id", "canonical_alias", "topic"]}
 | 
					      omit={["room_id", "canonical_alias", "topic"]}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
| 
						 | 
					@ -158,46 +146,18 @@ export const RoomDirectoryList = () => (
 | 
				
			||||||
        sx={{ height: "40px", width: "40px" }}
 | 
					        sx={{ height: "40px", width: "40px" }}
 | 
				
			||||||
        label="resources.rooms.fields.avatar"
 | 
					        label="resources.rooms.fields.avatar"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <TextField
 | 
					      <TextField source="name" sortable={false} label="resources.rooms.fields.name" />
 | 
				
			||||||
        source="name"
 | 
					      <TextField source="room_id" sortable={false} label="resources.rooms.fields.room_id" />
 | 
				
			||||||
        sortable={false}
 | 
					      <TextField source="canonical_alias" sortable={false} label="resources.rooms.fields.canonical_alias" />
 | 
				
			||||||
        label="resources.rooms.fields.name"
 | 
					      <TextField source="topic" sortable={false} label="resources.rooms.fields.topic" />
 | 
				
			||||||
      />
 | 
					      <NumberField source="num_joined_members" sortable={false} label="resources.rooms.fields.joined_members" />
 | 
				
			||||||
      <TextField
 | 
					      <BooleanField source="world_readable" sortable={false} label="resources.room_directory.fields.world_readable" />
 | 
				
			||||||
        source="room_id"
 | 
					      <BooleanField source="guest_can_join" sortable={false} label="resources.room_directory.fields.guest_can_join" />
 | 
				
			||||||
        sortable={false}
 | 
					 | 
				
			||||||
        label="resources.rooms.fields.room_id"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <TextField
 | 
					 | 
				
			||||||
        source="canonical_alias"
 | 
					 | 
				
			||||||
        sortable={false}
 | 
					 | 
				
			||||||
        label="resources.rooms.fields.canonical_alias"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <TextField
 | 
					 | 
				
			||||||
        source="topic"
 | 
					 | 
				
			||||||
        sortable={false}
 | 
					 | 
				
			||||||
        label="resources.rooms.fields.topic"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <NumberField
 | 
					 | 
				
			||||||
        source="num_joined_members"
 | 
					 | 
				
			||||||
        sortable={false}
 | 
					 | 
				
			||||||
        label="resources.rooms.fields.joined_members"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <BooleanField
 | 
					 | 
				
			||||||
        source="world_readable"
 | 
					 | 
				
			||||||
        sortable={false}
 | 
					 | 
				
			||||||
        label="resources.room_directory.fields.world_readable"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <BooleanField
 | 
					 | 
				
			||||||
        source="guest_can_join"
 | 
					 | 
				
			||||||
        sortable={false}
 | 
					 | 
				
			||||||
        label="resources.room_directory.fields.guest_can_join"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </DatagridConfigurable>
 | 
					    </DatagridConfigurable>
 | 
				
			||||||
  </List>
 | 
					  </List>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const resource = {
 | 
					const resource: ResourceProps = {
 | 
				
			||||||
  name: "room_directory",
 | 
					  name: "room_directory",
 | 
				
			||||||
  icon: RoomDirectoryIcon,
 | 
					  icon: RoomDirectoryIcon,
 | 
				
			||||||
  list: RoomDirectoryList,
 | 
					  list: RoomDirectoryList,
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,16 @@
 | 
				
			||||||
import React, { useState } from "react";
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import IconCancel from "@mui/icons-material/Cancel";
 | 
				
			||||||
 | 
					import MessageIcon from "@mui/icons-material/Message";
 | 
				
			||||||
 | 
					import { Dialog, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
 | 
					  RaRecord,
 | 
				
			||||||
  SaveButton,
 | 
					  SaveButton,
 | 
				
			||||||
  SimpleForm,
 | 
					  SimpleForm,
 | 
				
			||||||
  TextInput,
 | 
					  TextInput,
 | 
				
			||||||
  Toolbar,
 | 
					  Toolbar,
 | 
				
			||||||
 | 
					  ToolbarProps,
 | 
				
			||||||
  required,
 | 
					  required,
 | 
				
			||||||
  useCreate,
 | 
					  useCreate,
 | 
				
			||||||
  useDataProvider,
 | 
					  useDataProvider,
 | 
				
			||||||
| 
						 | 
					@ -15,24 +21,13 @@ import {
 | 
				
			||||||
  useUnselectAll,
 | 
					  useUnselectAll,
 | 
				
			||||||
} from "react-admin";
 | 
					} from "react-admin";
 | 
				
			||||||
import { useMutation } from "react-query";
 | 
					import { useMutation } from "react-query";
 | 
				
			||||||
import MessageIcon from "@mui/icons-material/Message";
 | 
					 | 
				
			||||||
import IconCancel from "@mui/icons-material/Cancel";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  Dialog,
 | 
					 | 
				
			||||||
  DialogContent,
 | 
					 | 
				
			||||||
  DialogContentText,
 | 
					 | 
				
			||||||
  DialogTitle,
 | 
					 | 
				
			||||||
} from "@mui/material";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => {
 | 
					const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ServerNoticeToolbar = props => (
 | 
					  const ServerNoticeToolbar = (props: ToolbarProps & { pristine?: boolean }) => (
 | 
				
			||||||
    <Toolbar {...props}>
 | 
					    <Toolbar {...props}>
 | 
				
			||||||
      <SaveButton
 | 
					      <SaveButton label="resources.servernotices.action.send" disabled={props.pristine} />
 | 
				
			||||||
        label="resources.servernotices.action.send"
 | 
					 | 
				
			||||||
        disabled={props.pristine}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <Button label="ra.action.cancel" onClick={onClose}>
 | 
					      <Button label="ra.action.cancel" onClick={onClose}>
 | 
				
			||||||
        <IconCancel />
 | 
					        <IconCancel />
 | 
				
			||||||
      </Button>
 | 
					      </Button>
 | 
				
			||||||
| 
						 | 
					@ -40,14 +35,10 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Dialog open={open} onClose={onClose} loading={loading}>
 | 
					    <Dialog open={open} onClose={onClose}>
 | 
				
			||||||
      <DialogTitle>
 | 
					      <DialogTitle>{translate("resources.servernotices.action.send")}</DialogTitle>
 | 
				
			||||||
        {translate("resources.servernotices.action.send")}
 | 
					 | 
				
			||||||
      </DialogTitle>
 | 
					 | 
				
			||||||
      <DialogContent>
 | 
					      <DialogContent>
 | 
				
			||||||
        <DialogContentText>
 | 
					        <DialogContentText>{translate("resources.servernotices.helper.send")}</DialogContentText>
 | 
				
			||||||
          {translate("resources.servernotices.helper.send")}
 | 
					 | 
				
			||||||
        </DialogContentText>
 | 
					 | 
				
			||||||
        <SimpleForm toolbar={<ServerNoticeToolbar />} onSubmit={onSubmit}>
 | 
					        <SimpleForm toolbar={<ServerNoticeToolbar />} onSubmit={onSubmit}>
 | 
				
			||||||
          <TextInput
 | 
					          <TextInput
 | 
				
			||||||
            source="body"
 | 
					            source="body"
 | 
				
			||||||
| 
						 | 
					@ -68,12 +59,12 @@ export const ServerNoticeButton = () => {
 | 
				
			||||||
  const record = useRecordContext();
 | 
					  const record = useRecordContext();
 | 
				
			||||||
  const [open, setOpen] = useState(false);
 | 
					  const [open, setOpen] = useState(false);
 | 
				
			||||||
  const notify = useNotify();
 | 
					  const notify = useNotify();
 | 
				
			||||||
  const [create, { isloading }] = useCreate();
 | 
					  const [create, { isLoading }] = useCreate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleDialogOpen = () => setOpen(true);
 | 
					  const handleDialogOpen = () => setOpen(true);
 | 
				
			||||||
  const handleDialogClose = () => setOpen(false);
 | 
					  const handleDialogClose = () => setOpen(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSend = values => {
 | 
					  const handleSend = (values: Partial<RaRecord>) => {
 | 
				
			||||||
    create(
 | 
					    create(
 | 
				
			||||||
      "servernotices",
 | 
					      "servernotices",
 | 
				
			||||||
      { data: { id: record.id, ...values } },
 | 
					      { data: { id: record.id, ...values } },
 | 
				
			||||||
| 
						 | 
					@ -92,18 +83,10 @@ export const ServerNoticeButton = () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <Button
 | 
					      <Button label="resources.servernotices.send" onClick={handleDialogOpen} disabled={isLoading}>
 | 
				
			||||||
        label="resources.servernotices.send"
 | 
					 | 
				
			||||||
        onClick={handleDialogOpen}
 | 
					 | 
				
			||||||
        disabled={isloading}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <MessageIcon />
 | 
					        <MessageIcon />
 | 
				
			||||||
      </Button>
 | 
					      </Button>
 | 
				
			||||||
      <ServerNoticeDialog
 | 
					      <ServerNoticeDialog open={open} onClose={handleDialogClose} onSubmit={handleSend} />
 | 
				
			||||||
        open={open}
 | 
					 | 
				
			||||||
        onClose={handleDialogClose}
 | 
					 | 
				
			||||||
        onSubmit={handleSend}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -138,18 +121,10 @@ export const ServerNoticeBulkButton = () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <Button
 | 
					      <Button label="resources.servernotices.send" onClick={openDialog} disabled={isLoading}>
 | 
				
			||||||
        label="resources.servernotices.send"
 | 
					 | 
				
			||||||
        onClick={openDialog}
 | 
					 | 
				
			||||||
        disabled={isLoading}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <MessageIcon />
 | 
					        <MessageIcon />
 | 
				
			||||||
      </Button>
 | 
					      </Button>
 | 
				
			||||||
      <ServerNoticeDialog
 | 
					      <ServerNoticeDialog open={open} onClose={closeDialog} onSubmit={sendNotices} />
 | 
				
			||||||
        open={open}
 | 
					 | 
				
			||||||
        onClose={closeDialog}
 | 
					 | 
				
			||||||
        onSubmit={sendNotices}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
							
								
								
									
										28
									
								
								src/components/date.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/components/date.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					export const DATE_FORMAT: Intl.DateTimeFormatOptions = {
 | 
				
			||||||
 | 
					  year: "numeric",
 | 
				
			||||||
 | 
					  month: "2-digit",
 | 
				
			||||||
 | 
					  day: "2-digit",
 | 
				
			||||||
 | 
					  hour: "2-digit",
 | 
				
			||||||
 | 
					  minute: "2-digit",
 | 
				
			||||||
 | 
					  second: "2-digit",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const dateParser = (v: string | number | Date): number => {
 | 
				
			||||||
 | 
					  const d = new Date(v);
 | 
				
			||||||
 | 
					  return d.getTime();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const dateFormatter = (v: string | number | Date | undefined | null): string => {
 | 
				
			||||||
 | 
					  if (v === undefined || v === null) return "";
 | 
				
			||||||
 | 
					  const d = new Date(v);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const pad = "00";
 | 
				
			||||||
 | 
					  const year = d.getFullYear().toString();
 | 
				
			||||||
 | 
					  const month = (pad + (d.getMonth() + 1).toString()).slice(-2);
 | 
				
			||||||
 | 
					  const day = (pad + d.getDate().toString()).slice(-2);
 | 
				
			||||||
 | 
					  const hour = (pad + d.getHours().toString()).slice(-2);
 | 
				
			||||||
 | 
					  const minute = (pad + d.getMinutes().toString()).slice(-2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // target format yyyy-MM-ddThh:mm
 | 
				
			||||||
 | 
					  return `${year}-${month}-${day}T${hour}:${minute}`;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,23 @@
 | 
				
			||||||
import React from "react";
 | 
					import { MouseEvent } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import AutorenewIcon from "@mui/icons-material/Autorenew";
 | 
				
			||||||
 | 
					import DestinationsIcon from "@mui/icons-material/CloudQueue";
 | 
				
			||||||
 | 
					import FolderSharedIcon from "@mui/icons-material/FolderShared";
 | 
				
			||||||
 | 
					import ViewListIcon from "@mui/icons-material/ViewList";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
  Datagrid,
 | 
					  Datagrid,
 | 
				
			||||||
  DateField,
 | 
					  DateField,
 | 
				
			||||||
  List,
 | 
					  List,
 | 
				
			||||||
 | 
					  ListProps,
 | 
				
			||||||
  Pagination,
 | 
					  Pagination,
 | 
				
			||||||
 | 
					  RaRecord,
 | 
				
			||||||
  ReferenceField,
 | 
					  ReferenceField,
 | 
				
			||||||
  ReferenceManyField,
 | 
					  ReferenceManyField,
 | 
				
			||||||
 | 
					  ResourceProps,
 | 
				
			||||||
  SearchInput,
 | 
					  SearchInput,
 | 
				
			||||||
  Show,
 | 
					  Show,
 | 
				
			||||||
 | 
					  ShowProps,
 | 
				
			||||||
  Tab,
 | 
					  Tab,
 | 
				
			||||||
  TabbedShowLayout,
 | 
					  TabbedShowLayout,
 | 
				
			||||||
  TextField,
 | 
					  TextField,
 | 
				
			||||||
| 
						 | 
					@ -19,25 +28,12 @@ import {
 | 
				
			||||||
  useRefresh,
 | 
					  useRefresh,
 | 
				
			||||||
  useTranslate,
 | 
					  useTranslate,
 | 
				
			||||||
} from "react-admin";
 | 
					} from "react-admin";
 | 
				
			||||||
import AutorenewIcon from "@mui/icons-material/Autorenew";
 | 
					 | 
				
			||||||
import DestinationsIcon from "@mui/icons-material/CloudQueue";
 | 
					 | 
				
			||||||
import FolderSharedIcon from "@mui/icons-material/FolderShared";
 | 
					 | 
				
			||||||
import ViewListIcon from "@mui/icons-material/ViewList";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DestinationPagination = () => (
 | 
					import { DATE_FORMAT } from "./date";
 | 
				
			||||||
  <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const date_format = {
 | 
					const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
 | 
				
			||||||
  year: "numeric",
 | 
					 | 
				
			||||||
  month: "2-digit",
 | 
					 | 
				
			||||||
  day: "2-digit",
 | 
					 | 
				
			||||||
  hour: "2-digit",
 | 
					 | 
				
			||||||
  minute: "2-digit",
 | 
					 | 
				
			||||||
  second: "2-digit",
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const destinationRowSx = (record, _index) => ({
 | 
					const destinationRowSx = (record: RaRecord) => ({
 | 
				
			||||||
  backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
 | 
					  backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,7 +48,7 @@ export const DestinationReconnectButton = () => {
 | 
				
			||||||
  // Reconnect is not required if no error has occurred. (`failure_ts`)
 | 
					  // Reconnect is not required if no error has occurred. (`failure_ts`)
 | 
				
			||||||
  if (!record || !record.failure_ts) return null;
 | 
					  if (!record || !record.failure_ts) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleClick = e => {
 | 
					  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
 | 
				
			||||||
    // Prevents redirection to the detail page when clicking in the list
 | 
					    // Prevents redirection to the detail page when clicking in the list
 | 
				
			||||||
    e.stopPropagation();
 | 
					    e.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -74,11 +70,7 @@ export const DestinationReconnectButton = () => {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Button
 | 
					    <Button label="resources.destinations.action.reconnect" onClick={handleClick} disabled={isLoading}>
 | 
				
			||||||
      label="resources.destinations.action.reconnect"
 | 
					 | 
				
			||||||
      onClick={handleClick}
 | 
					 | 
				
			||||||
      disabled={isLoading}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <AutorenewIcon />
 | 
					      <AutorenewIcon />
 | 
				
			||||||
    </Button>
 | 
					    </Button>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
| 
						 | 
					@ -100,7 +92,7 @@ const DestinationTitle = () => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DestinationList = props => {
 | 
					export const DestinationList = (props: ListProps) => {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <List
 | 
					    <List
 | 
				
			||||||
      {...props}
 | 
					      {...props}
 | 
				
			||||||
| 
						 | 
					@ -108,14 +100,10 @@ export const DestinationList = props => {
 | 
				
			||||||
      pagination={<DestinationPagination />}
 | 
					      pagination={<DestinationPagination />}
 | 
				
			||||||
      sort={{ field: "destination", order: "ASC" }}
 | 
					      sort={{ field: "destination", order: "ASC" }}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <Datagrid
 | 
					      <Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
 | 
				
			||||||
        rowSx={destinationRowSx}
 | 
					 | 
				
			||||||
        rowClick={(id, _resource, _record) => `${id}/show/rooms`}
 | 
					 | 
				
			||||||
        bulkActionButtons={false}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <TextField source="destination" />
 | 
					        <TextField source="destination" />
 | 
				
			||||||
        <DateField source="failure_ts" showTime options={date_format} />
 | 
					        <DateField source="failure_ts" showTime options={DATE_FORMAT} />
 | 
				
			||||||
        <DateField source="retry_last_ts" showTime options={date_format} />
 | 
					        <DateField source="retry_last_ts" showTime options={DATE_FORMAT} />
 | 
				
			||||||
        <TextField source="retry_interval" />
 | 
					        <TextField source="retry_interval" />
 | 
				
			||||||
        <TextField source="last_successful_stream_ordering" />
 | 
					        <TextField source="last_successful_stream_ordering" />
 | 
				
			||||||
        <DestinationReconnectButton />
 | 
					        <DestinationReconnectButton />
 | 
				
			||||||
| 
						 | 
					@ -124,43 +112,29 @@ export const DestinationList = props => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DestinationShow = props => {
 | 
					export const DestinationShow = (props: ShowProps) => {
 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Show
 | 
					    <Show actions={<DestinationShowActions />} title={<DestinationTitle />} {...props}>
 | 
				
			||||||
      actions={<DestinationShowActions />}
 | 
					 | 
				
			||||||
      title={<DestinationTitle />}
 | 
					 | 
				
			||||||
      {...props}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <TabbedShowLayout>
 | 
					      <TabbedShowLayout>
 | 
				
			||||||
        <Tab label="status" icon={<ViewListIcon />}>
 | 
					        <Tab label="status" icon={<ViewListIcon />}>
 | 
				
			||||||
          <TextField source="destination" />
 | 
					          <TextField source="destination" />
 | 
				
			||||||
          <DateField source="failure_ts" showTime options={date_format} />
 | 
					          <DateField source="failure_ts" showTime options={DATE_FORMAT} />
 | 
				
			||||||
          <DateField source="retry_last_ts" showTime options={date_format} />
 | 
					          <DateField source="retry_last_ts" showTime options={DATE_FORMAT} />
 | 
				
			||||||
          <TextField source="retry_interval" />
 | 
					          <TextField source="retry_interval" />
 | 
				
			||||||
          <TextField source="last_successful_stream_ordering" />
 | 
					          <TextField source="last_successful_stream_ordering" />
 | 
				
			||||||
        </Tab>
 | 
					        </Tab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Tab
 | 
					        <Tab label={translate("resources.rooms.name", { smart_count: 2 })} icon={<FolderSharedIcon />} path="rooms">
 | 
				
			||||||
          label={translate("resources.rooms.name", { smart_count: 2 })}
 | 
					 | 
				
			||||||
          icon={<FolderSharedIcon />}
 | 
					 | 
				
			||||||
          path="rooms"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <ReferenceManyField
 | 
					          <ReferenceManyField
 | 
				
			||||||
            reference="destination_rooms"
 | 
					            reference="destination_rooms"
 | 
				
			||||||
            target="destination"
 | 
					            target="destination"
 | 
				
			||||||
            addLabel={false}
 | 
					            label={false}
 | 
				
			||||||
            pagination={<DestinationPagination />}
 | 
					            pagination={<DestinationPagination />}
 | 
				
			||||||
            perPage={50}
 | 
					            perPage={50}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <Datagrid
 | 
					            <Datagrid style={{ width: "100%" }} rowClick={id => `/rooms/${id}/show`}>
 | 
				
			||||||
              style={{ width: "100%" }}
 | 
					              <TextField source="room_id" label="resources.rooms.fields.room_id" />
 | 
				
			||||||
              rowClick={(id, resource, record) => `/rooms/${id}/show`}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <TextField
 | 
					 | 
				
			||||||
                source="room_id"
 | 
					 | 
				
			||||||
                label="resources.rooms.fields.room_id"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <TextField source="stream_ordering" sortable={false} />
 | 
					              <TextField source="stream_ordering" sortable={false} />
 | 
				
			||||||
              <ReferenceField
 | 
					              <ReferenceField
 | 
				
			||||||
                label="resources.rooms.fields.name"
 | 
					                label="resources.rooms.fields.name"
 | 
				
			||||||
| 
						 | 
					@ -179,7 +153,7 @@ export const DestinationShow = props => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const resource = {
 | 
					const resource: ResourceProps = {
 | 
				
			||||||
  name: "destinations",
 | 
					  name: "destinations",
 | 
				
			||||||
  icon: DestinationsIcon,
 | 
					  icon: DestinationsIcon,
 | 
				
			||||||
  list: DestinationList,
 | 
					  list: DestinationList,
 | 
				
			||||||
| 
						 | 
					@ -1,51 +0,0 @@
 | 
				
			||||||
import React from "react";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  DeleteButton,
 | 
					 | 
				
			||||||
  useDelete,
 | 
					 | 
				
			||||||
  useNotify,
 | 
					 | 
				
			||||||
  useRecordContext,
 | 
					 | 
				
			||||||
  useRefresh,
 | 
					 | 
				
			||||||
} from "react-admin";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const DeviceRemoveButton = props => {
 | 
					 | 
				
			||||||
  const record = useRecordContext();
 | 
					 | 
				
			||||||
  const refresh = useRefresh();
 | 
					 | 
				
			||||||
  const notify = useNotify();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const [removeDevice] = useDelete();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!record) return null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleConfirm = () => {
 | 
					 | 
				
			||||||
    removeDevice(
 | 
					 | 
				
			||||||
      "devices",
 | 
					 | 
				
			||||||
      // needs previousData for user_id
 | 
					 | 
				
			||||||
      { id: record.id, previousData: record },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        onSuccess: () => {
 | 
					 | 
				
			||||||
          notify("resources.devices.action.erase.success");
 | 
					 | 
				
			||||||
          refresh();
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        onError: () => {
 | 
					 | 
				
			||||||
          notify("resources.devices.action.erase.failure", { type: "error" });
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <DeleteButton
 | 
					 | 
				
			||||||
      {...props}
 | 
					 | 
				
			||||||
      label="ra.action.remove"
 | 
					 | 
				
			||||||
      confirmTitle="resources.devices.action.erase.title"
 | 
					 | 
				
			||||||
      confirmContent="resources.devices.action.erase.content"
 | 
					 | 
				
			||||||
      onConfirm={handleConfirm}
 | 
					 | 
				
			||||||
      mutationMode="pessimistic"
 | 
					 | 
				
			||||||
      redirect={false}
 | 
					 | 
				
			||||||
      translateOptions={{
 | 
					 | 
				
			||||||
        id: record.id,
 | 
					 | 
				
			||||||
        name: record.display_name ? record.display_name : record.id,
 | 
					 | 
				
			||||||
      }}
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
							
								
								
									
										21
									
								
								src/components/devices.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/components/devices.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
 | 
				
			||||||
 | 
					  const record = useRecordContext();
 | 
				
			||||||
 | 
					  if (!record) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <DeleteWithConfirmButton
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					      label="ra.action.remove"
 | 
				
			||||||
 | 
					      confirmTitle="resources.devices.action.erase.title"
 | 
				
			||||||
 | 
					      confirmContent="resources.devices.action.erase.content"
 | 
				
			||||||
 | 
					      mutationMode="pessimistic"
 | 
				
			||||||
 | 
					      redirect={false}
 | 
				
			||||||
 | 
					      translateOptions={{
 | 
				
			||||||
 | 
					        id: record.id,
 | 
				
			||||||
 | 
					        name: record.display_name ? record.display_name : record.id,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,25 @@
 | 
				
			||||||
import React, { useState } from "react";
 | 
					import { get } from "lodash";
 | 
				
			||||||
import get from "lodash/get";
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import BlockIcon from "@mui/icons-material/Block";
 | 
				
			||||||
 | 
					import IconCancel from "@mui/icons-material/Cancel";
 | 
				
			||||||
 | 
					import ClearIcon from "@mui/icons-material/Clear";
 | 
				
			||||||
 | 
					import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
 | 
				
			||||||
 | 
					import FileOpenIcon from "@mui/icons-material/FileOpen";
 | 
				
			||||||
 | 
					import LockIcon from "@mui/icons-material/Lock";
 | 
				
			||||||
 | 
					import LockOpenIcon from "@mui/icons-material/LockOpen";
 | 
				
			||||||
 | 
					import { Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip } from "@mui/material";
 | 
				
			||||||
 | 
					import { alpha, useTheme } from "@mui/material/styles";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  BooleanInput,
 | 
					  BooleanInput,
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
 | 
					  ButtonProps,
 | 
				
			||||||
  DateTimeInput,
 | 
					  DateTimeInput,
 | 
				
			||||||
  NumberInput,
 | 
					  NumberInput,
 | 
				
			||||||
  SaveButton,
 | 
					  SaveButton,
 | 
				
			||||||
  SimpleForm,
 | 
					  SimpleForm,
 | 
				
			||||||
  Toolbar,
 | 
					  Toolbar,
 | 
				
			||||||
 | 
					  ToolbarProps,
 | 
				
			||||||
  useCreate,
 | 
					  useCreate,
 | 
				
			||||||
  useDelete,
 | 
					  useDelete,
 | 
				
			||||||
  useNotify,
 | 
					  useNotify,
 | 
				
			||||||
| 
						 | 
					@ -16,39 +28,16 @@ import {
 | 
				
			||||||
  useTranslate,
 | 
					  useTranslate,
 | 
				
			||||||
} from "react-admin";
 | 
					} from "react-admin";
 | 
				
			||||||
import { Link } from "react-router-dom";
 | 
					import { Link } from "react-router-dom";
 | 
				
			||||||
import BlockIcon from "@mui/icons-material/Block";
 | 
					
 | 
				
			||||||
import ClearIcon from "@mui/icons-material/Clear";
 | 
					import { dateParser } from "./date";
 | 
				
			||||||
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  Box,
 | 
					 | 
				
			||||||
  Dialog,
 | 
					 | 
				
			||||||
  DialogContent,
 | 
					 | 
				
			||||||
  DialogContentText,
 | 
					 | 
				
			||||||
  DialogTitle,
 | 
					 | 
				
			||||||
  Tooltip,
 | 
					 | 
				
			||||||
} from "@mui/material";
 | 
					 | 
				
			||||||
import IconCancel from "@mui/icons-material/Cancel";
 | 
					 | 
				
			||||||
import LockIcon from "@mui/icons-material/Lock";
 | 
					 | 
				
			||||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
 | 
					 | 
				
			||||||
import FileOpenIcon from "@mui/icons-material/FileOpen";
 | 
					 | 
				
			||||||
import { alpha, useTheme } from "@mui/material/styles";
 | 
					 | 
				
			||||||
import { getMediaUrl } from "../synapse/synapse";
 | 
					import { getMediaUrl } from "../synapse/synapse";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
 | 
					const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const dateParser = v => {
 | 
					  const DeleteMediaToolbar = (props: ToolbarProps) => (
 | 
				
			||||||
    const d = new Date(v);
 | 
					 | 
				
			||||||
    if (isNaN(d)) return 0;
 | 
					 | 
				
			||||||
    return d.getTime();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const DeleteMediaToolbar = props => (
 | 
					 | 
				
			||||||
    <Toolbar {...props}>
 | 
					    <Toolbar {...props}>
 | 
				
			||||||
      <SaveButton
 | 
					      <SaveButton label="resources.delete_media.action.send" icon={<DeleteSweepIcon />} />
 | 
				
			||||||
        label="resources.delete_media.action.send"
 | 
					 | 
				
			||||||
        icon={<DeleteSweepIcon />}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <Button label="ra.action.cancel" onClick={onClose}>
 | 
					      <Button label="ra.action.cancel" onClick={onClose}>
 | 
				
			||||||
        <IconCancel />
 | 
					        <IconCancel />
 | 
				
			||||||
      </Button>
 | 
					      </Button>
 | 
				
			||||||
| 
						 | 
					@ -56,14 +45,10 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Dialog open={open} onClose={onClose} loading={loading}>
 | 
					    <Dialog open={open} onClose={onClose}>
 | 
				
			||||||
      <DialogTitle>
 | 
					      <DialogTitle>{translate("resources.delete_media.action.send")}</DialogTitle>
 | 
				
			||||||
        {translate("resources.delete_media.action.send")}
 | 
					 | 
				
			||||||
      </DialogTitle>
 | 
					 | 
				
			||||||
      <DialogContent>
 | 
					      <DialogContent>
 | 
				
			||||||
        <DialogContentText>
 | 
					        <DialogContentText>{translate("resources.delete_media.helper.send")}</DialogContentText>
 | 
				
			||||||
          {translate("resources.delete_media.helper.send")}
 | 
					 | 
				
			||||||
        </DialogContentText>
 | 
					 | 
				
			||||||
        <SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
 | 
					        <SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
 | 
				
			||||||
          <DateTimeInput
 | 
					          <DateTimeInput
 | 
				
			||||||
            fullWidth
 | 
					            fullWidth
 | 
				
			||||||
| 
						 | 
					@ -92,7 +77,7 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DeleteMediaButton = props => {
 | 
					export const DeleteMediaButton = (props: ButtonProps) => {
 | 
				
			||||||
  const theme = useTheme();
 | 
					  const theme = useTheme();
 | 
				
			||||||
  const [open, setOpen] = useState(false);
 | 
					  const [open, setOpen] = useState(false);
 | 
				
			||||||
  const notify = useNotify();
 | 
					  const notify = useNotify();
 | 
				
			||||||
| 
						 | 
					@ -101,7 +86,7 @@ export const DeleteMediaButton = props => {
 | 
				
			||||||
  const openDialog = () => setOpen(true);
 | 
					  const openDialog = () => setOpen(true);
 | 
				
			||||||
  const closeDialog = () => setOpen(false);
 | 
					  const closeDialog = () => setOpen(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const deleteMedia = values => {
 | 
					  const deleteMedia = (values: { before_ts: string; size_gt: number; keep_profiles: boolean }) => {
 | 
				
			||||||
    deleteOne(
 | 
					    deleteOne(
 | 
				
			||||||
      "delete_media",
 | 
					      "delete_media",
 | 
				
			||||||
      // needs meta.before_ts, meta.size_gt and meta.keep_profiles
 | 
					      // needs meta.before_ts, meta.size_gt and meta.keep_profiles
 | 
				
			||||||
| 
						 | 
					@ -139,16 +124,12 @@ export const DeleteMediaButton = props => {
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <DeleteSweepIcon />
 | 
					        <DeleteSweepIcon />
 | 
				
			||||||
      </Button>
 | 
					      </Button>
 | 
				
			||||||
      <DeleteMediaDialog
 | 
					      <DeleteMediaDialog open={open} onClose={closeDialog} onSubmit={deleteMedia} />
 | 
				
			||||||
        open={open}
 | 
					 | 
				
			||||||
        onClose={closeDialog}
 | 
					 | 
				
			||||||
        onSubmit={deleteMedia}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ProtectMediaButton = () => {
 | 
					export const ProtectMediaButton = (props: ButtonProps) => {
 | 
				
			||||||
  const record = useRecordContext();
 | 
					  const record = useRecordContext();
 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
  const refresh = useRefresh();
 | 
					  const refresh = useRefresh();
 | 
				
			||||||
| 
						 | 
					@ -209,7 +190,7 @@ export const ProtectMediaButton = () => {
 | 
				
			||||||
            Button instead BooleanField for
 | 
					            Button instead BooleanField for
 | 
				
			||||||
            consistent appearance and position in the column
 | 
					            consistent appearance and position in the column
 | 
				
			||||||
            */}
 | 
					            */}
 | 
				
			||||||
            <Button disabled={true}>
 | 
					            <Button {...props} disabled={true}>
 | 
				
			||||||
              <ClearIcon />
 | 
					              <ClearIcon />
 | 
				
			||||||
            </Button>
 | 
					            </Button>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
| 
						 | 
					@ -223,7 +204,7 @@ export const ProtectMediaButton = () => {
 | 
				
			||||||
          arrow
 | 
					          arrow
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
            <Button onClick={handleUnprotect} disabled={isLoading}>
 | 
					            <Button {...props} onClick={handleUnprotect} disabled={isLoading}>
 | 
				
			||||||
              <LockIcon />
 | 
					              <LockIcon />
 | 
				
			||||||
            </Button>
 | 
					            </Button>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
| 
						 | 
					@ -236,7 +217,7 @@ export const ProtectMediaButton = () => {
 | 
				
			||||||
          })}
 | 
					          })}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
            <Button onClick={handleProtect} disabled={isLoading}>
 | 
					            <Button {...props} onClick={handleProtect} disabled={isLoading}>
 | 
				
			||||||
              <LockOpenIcon />
 | 
					              <LockOpenIcon />
 | 
				
			||||||
            </Button>
 | 
					            </Button>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
| 
						 | 
					@ -246,7 +227,7 @@ export const ProtectMediaButton = () => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const QuarantineMediaButton = props => {
 | 
					export const QuarantineMediaButton = (props: ButtonProps) => {
 | 
				
			||||||
  const record = useRecordContext();
 | 
					  const record = useRecordContext();
 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
  const refresh = useRefresh();
 | 
					  const refresh = useRefresh();
 | 
				
			||||||
| 
						 | 
					@ -312,11 +293,7 @@ export const QuarantineMediaButton = props => {
 | 
				
			||||||
          })}
 | 
					          })}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
            <Button
 | 
					            <Button {...props} onClick={handleRemoveQuarantaine} disabled={isLoading}>
 | 
				
			||||||
              {...props}
 | 
					 | 
				
			||||||
              onClick={handleRemoveQuarantaine}
 | 
					 | 
				
			||||||
              disabled={isLoading}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <BlockIcon color="error" />
 | 
					              <BlockIcon color="error" />
 | 
				
			||||||
            </Button>
 | 
					            </Button>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
| 
						 | 
					@ -329,7 +306,7 @@ export const QuarantineMediaButton = props => {
 | 
				
			||||||
          })}
 | 
					          })}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
            <Button onClick={handleQuarantaine} disabled={isLoading}>
 | 
					            <Button {...props} onClick={handleQuarantaine} disabled={isLoading}>
 | 
				
			||||||
              <BlockIcon />
 | 
					              <BlockIcon />
 | 
				
			||||||
            </Button>
 | 
					            </Button>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,14 @@
 | 
				
			||||||
import React from "react";
 | 
					import EventIcon from "@mui/icons-material/Event";
 | 
				
			||||||
 | 
					import FastForwardIcon from "@mui/icons-material/FastForward";
 | 
				
			||||||
 | 
					import UserIcon from "@mui/icons-material/Group";
 | 
				
			||||||
 | 
					import HttpsIcon from "@mui/icons-material/Https";
 | 
				
			||||||
 | 
					import NoEncryptionIcon from "@mui/icons-material/NoEncryption";
 | 
				
			||||||
 | 
					import PageviewIcon from "@mui/icons-material/Pageview";
 | 
				
			||||||
 | 
					import ViewListIcon from "@mui/icons-material/ViewList";
 | 
				
			||||||
 | 
					import RoomIcon from "@mui/icons-material/ViewList";
 | 
				
			||||||
 | 
					import VisibilityIcon from "@mui/icons-material/Visibility";
 | 
				
			||||||
 | 
					import Box from "@mui/material/Box";
 | 
				
			||||||
 | 
					import { useTheme } from "@mui/material/styles";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  BooleanField,
 | 
					  BooleanField,
 | 
				
			||||||
  BulkDeleteButton,
 | 
					  BulkDeleteButton,
 | 
				
			||||||
| 
						 | 
					@ -9,14 +19,17 @@ import {
 | 
				
			||||||
  ExportButton,
 | 
					  ExportButton,
 | 
				
			||||||
  FunctionField,
 | 
					  FunctionField,
 | 
				
			||||||
  List,
 | 
					  List,
 | 
				
			||||||
 | 
					  ListProps,
 | 
				
			||||||
  NumberField,
 | 
					  NumberField,
 | 
				
			||||||
  Pagination,
 | 
					  Pagination,
 | 
				
			||||||
  ReferenceField,
 | 
					  ReferenceField,
 | 
				
			||||||
  ReferenceManyField,
 | 
					  ReferenceManyField,
 | 
				
			||||||
 | 
					  ResourceProps,
 | 
				
			||||||
  SearchInput,
 | 
					  SearchInput,
 | 
				
			||||||
  SelectColumnsButton,
 | 
					  SelectColumnsButton,
 | 
				
			||||||
  SelectField,
 | 
					  SelectField,
 | 
				
			||||||
  Show,
 | 
					  Show,
 | 
				
			||||||
 | 
					  ShowProps,
 | 
				
			||||||
  Tab,
 | 
					  Tab,
 | 
				
			||||||
  TabbedShowLayout,
 | 
					  TabbedShowLayout,
 | 
				
			||||||
  TextField,
 | 
					  TextField,
 | 
				
			||||||
| 
						 | 
					@ -24,41 +37,21 @@ import {
 | 
				
			||||||
  useRecordContext,
 | 
					  useRecordContext,
 | 
				
			||||||
  useTranslate,
 | 
					  useTranslate,
 | 
				
			||||||
} from "react-admin";
 | 
					} from "react-admin";
 | 
				
			||||||
import { useTheme } from "@mui/material/styles";
 | 
					
 | 
				
			||||||
import Box from "@mui/material/Box";
 | 
					 | 
				
			||||||
import FastForwardIcon from "@mui/icons-material/FastForward";
 | 
					 | 
				
			||||||
import HttpsIcon from "@mui/icons-material/Https";
 | 
					 | 
				
			||||||
import NoEncryptionIcon from "@mui/icons-material/NoEncryption";
 | 
					 | 
				
			||||||
import PageviewIcon from "@mui/icons-material/Pageview";
 | 
					 | 
				
			||||||
import UserIcon from "@mui/icons-material/Group";
 | 
					 | 
				
			||||||
import ViewListIcon from "@mui/icons-material/ViewList";
 | 
					 | 
				
			||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
 | 
					 | 
				
			||||||
import EventIcon from "@mui/icons-material/Event";
 | 
					 | 
				
			||||||
import RoomIcon from "@mui/icons-material/ViewList";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  RoomDirectoryBulkUnpublishButton,
 | 
					  RoomDirectoryBulkUnpublishButton,
 | 
				
			||||||
  RoomDirectoryBulkPublishButton,
 | 
					  RoomDirectoryBulkPublishButton,
 | 
				
			||||||
  RoomDirectoryUnpublishButton,
 | 
					  RoomDirectoryUnpublishButton,
 | 
				
			||||||
  RoomDirectoryPublishButton,
 | 
					  RoomDirectoryPublishButton,
 | 
				
			||||||
} from "./RoomDirectory";
 | 
					} from "./RoomDirectory";
 | 
				
			||||||
 | 
					import { DATE_FORMAT } from "./date";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const date_format = {
 | 
					const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
 | 
				
			||||||
  year: "numeric",
 | 
					 | 
				
			||||||
  month: "2-digit",
 | 
					 | 
				
			||||||
  day: "2-digit",
 | 
					 | 
				
			||||||
  hour: "2-digit",
 | 
					 | 
				
			||||||
  minute: "2-digit",
 | 
					 | 
				
			||||||
  second: "2-digit",
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const RoomPagination = () => (
 | 
					 | 
				
			||||||
  <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const RoomTitle = () => {
 | 
					const RoomTitle = () => {
 | 
				
			||||||
  const record = useRecordContext();
 | 
					  const record = useRecordContext();
 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
  var name = "";
 | 
					  let name = "";
 | 
				
			||||||
  if (record) {
 | 
					  if (record) {
 | 
				
			||||||
    name = record.name !== "" ? record.name : record.id;
 | 
					    name = record.name !== "" ? record.name : record.id;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -72,15 +65,11 @@ const RoomTitle = () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const RoomShowActions = () => {
 | 
					const RoomShowActions = () => {
 | 
				
			||||||
  const record = useRecordContext();
 | 
					  const record = useRecordContext();
 | 
				
			||||||
  var roomDirectoryStatus = "";
 | 
					  const publishButton = record.public ? <RoomDirectoryUnpublishButton /> : <RoomDirectoryPublishButton />;
 | 
				
			||||||
  if (record) {
 | 
					  // FIXME: refresh after (un)publish
 | 
				
			||||||
    roomDirectoryStatus = record.public;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <TopToolbar>
 | 
					    <TopToolbar>
 | 
				
			||||||
      {roomDirectoryStatus === false && <RoomDirectoryPublishButton />}
 | 
					      {publishButton}
 | 
				
			||||||
      {roomDirectoryStatus === true && <RoomDirectoryUnpublishButton />}
 | 
					 | 
				
			||||||
      <DeleteButton
 | 
					      <DeleteButton
 | 
				
			||||||
        mutationMode="pessimistic"
 | 
					        mutationMode="pessimistic"
 | 
				
			||||||
        confirmTitle="resources.rooms.action.erase.title"
 | 
					        confirmTitle="resources.rooms.action.erase.title"
 | 
				
			||||||
| 
						 | 
					@ -90,7 +79,7 @@ const RoomShowActions = () => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RoomShow = props => {
 | 
					export const RoomShow = (props: ShowProps) => {
 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
 | 
					    <Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
 | 
				
			||||||
| 
						 | 
					@ -105,42 +94,19 @@ export const RoomShow = props => {
 | 
				
			||||||
          </ReferenceField>
 | 
					          </ReferenceField>
 | 
				
			||||||
        </Tab>
 | 
					        </Tab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Tab
 | 
					        <Tab label="synapseadmin.rooms.tabs.detail" icon={<PageviewIcon />} path="detail">
 | 
				
			||||||
          label="synapseadmin.rooms.tabs.detail"
 | 
					 | 
				
			||||||
          icon={<PageviewIcon />}
 | 
					 | 
				
			||||||
          path="detail"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <TextField source="joined_members" />
 | 
					          <TextField source="joined_members" />
 | 
				
			||||||
          <TextField source="joined_local_members" />
 | 
					          <TextField source="joined_local_members" />
 | 
				
			||||||
          <TextField source="joined_local_devices" />
 | 
					          <TextField source="joined_local_devices" />
 | 
				
			||||||
          <TextField source="state_events" />
 | 
					          <TextField source="state_events" />
 | 
				
			||||||
          <TextField source="version" />
 | 
					          <TextField source="version" />
 | 
				
			||||||
          <TextField
 | 
					          <TextField source="encryption" emptyText={translate("resources.rooms.enums.unencrypted")} />
 | 
				
			||||||
            source="encryption"
 | 
					 | 
				
			||||||
            emptyText={translate("resources.rooms.enums.unencrypted")}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </Tab>
 | 
					        </Tab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Tab
 | 
					        <Tab label="synapseadmin.rooms.tabs.members" icon={<UserIcon />} path="members">
 | 
				
			||||||
          label="synapseadmin.rooms.tabs.members"
 | 
					          <ReferenceManyField reference="room_members" target="room_id" label={false}>
 | 
				
			||||||
          icon={<UserIcon />}
 | 
					            <Datagrid style={{ width: "100%" }} rowClick={id => "/users/" + id} bulkActionButtons={false}>
 | 
				
			||||||
          path="members"
 | 
					              <TextField source="id" sortable={false} label="resources.users.fields.id" />
 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <ReferenceManyField
 | 
					 | 
				
			||||||
            reference="room_members"
 | 
					 | 
				
			||||||
            target="room_id"
 | 
					 | 
				
			||||||
            addLabel={false}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Datagrid
 | 
					 | 
				
			||||||
              style={{ width: "100%" }}
 | 
					 | 
				
			||||||
              rowClick={(id, resource, record) => "/users/" + id}
 | 
					 | 
				
			||||||
              bulkActionButtons={false}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <TextField
 | 
					 | 
				
			||||||
                source="id"
 | 
					 | 
				
			||||||
                sortable={false}
 | 
					 | 
				
			||||||
                label="resources.users.fields.id"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <ReferenceField
 | 
					              <ReferenceField
 | 
				
			||||||
                label="resources.users.fields.displayname"
 | 
					                label="resources.users.fields.displayname"
 | 
				
			||||||
                source="id"
 | 
					                source="id"
 | 
				
			||||||
| 
						 | 
					@ -154,11 +120,7 @@ export const RoomShow = props => {
 | 
				
			||||||
          </ReferenceManyField>
 | 
					          </ReferenceManyField>
 | 
				
			||||||
        </Tab>
 | 
					        </Tab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Tab
 | 
					        <Tab label="synapseadmin.rooms.tabs.permission" icon={<VisibilityIcon />} path="permission">
 | 
				
			||||||
          label="synapseadmin.rooms.tabs.permission"
 | 
					 | 
				
			||||||
          icon={<VisibilityIcon />}
 | 
					 | 
				
			||||||
          path="permission"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <BooleanField source="federatable" />
 | 
					          <BooleanField source="federatable" />
 | 
				
			||||||
          <BooleanField source="public" />
 | 
					          <BooleanField source="public" />
 | 
				
			||||||
          <SelectField
 | 
					          <SelectField
 | 
				
			||||||
| 
						 | 
					@ -209,41 +171,20 @@ export const RoomShow = props => {
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </Tab>
 | 
					        </Tab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Tab
 | 
					        <Tab label={translate("resources.room_state.name", { smart_count: 2 })} icon={<EventIcon />} path="state">
 | 
				
			||||||
          label={translate("resources.room_state.name", { smart_count: 2 })}
 | 
					          <ReferenceManyField reference="room_state" target="room_id" label={false}>
 | 
				
			||||||
          icon={<EventIcon />}
 | 
					 | 
				
			||||||
          path="state"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <ReferenceManyField
 | 
					 | 
				
			||||||
            reference="room_state"
 | 
					 | 
				
			||||||
            target="room_id"
 | 
					 | 
				
			||||||
            addLabel={false}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
 | 
					            <Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
 | 
				
			||||||
              <TextField source="type" sortable={false} />
 | 
					              <TextField source="type" sortable={false} />
 | 
				
			||||||
              <DateField
 | 
					              <DateField source="origin_server_ts" showTime options={DATE_FORMAT} sortable={false} />
 | 
				
			||||||
                source="origin_server_ts"
 | 
					 | 
				
			||||||
                showTime
 | 
					 | 
				
			||||||
                options={date_format}
 | 
					 | 
				
			||||||
                sortable={false}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <TextField source="content" sortable={false} />
 | 
					              <TextField source="content" sortable={false} />
 | 
				
			||||||
              <ReferenceField
 | 
					              <ReferenceField source="sender" reference="users" sortable={false}>
 | 
				
			||||||
                source="sender"
 | 
					 | 
				
			||||||
                reference="users"
 | 
					 | 
				
			||||||
                sortable={false}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <TextField source="id" />
 | 
					                <TextField source="id" />
 | 
				
			||||||
              </ReferenceField>
 | 
					              </ReferenceField>
 | 
				
			||||||
            </Datagrid>
 | 
					            </Datagrid>
 | 
				
			||||||
          </ReferenceManyField>
 | 
					          </ReferenceManyField>
 | 
				
			||||||
        </Tab>
 | 
					        </Tab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Tab
 | 
					        <Tab label="resources.forward_extremities.name" icon={<FastForwardIcon />} path="forward_extremities">
 | 
				
			||||||
          label="resources.forward_extremities.name"
 | 
					 | 
				
			||||||
          icon={<FastForwardIcon />}
 | 
					 | 
				
			||||||
          path="forward_extremities"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <Box
 | 
					          <Box
 | 
				
			||||||
            sx={{
 | 
					            sx={{
 | 
				
			||||||
              fontFamily: "Roboto, Helvetica, Arial, sans-serif",
 | 
					              fontFamily: "Roboto, Helvetica, Arial, sans-serif",
 | 
				
			||||||
| 
						 | 
					@ -252,19 +193,10 @@ export const RoomShow = props => {
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            {translate("resources.rooms.helper.forward_extremities")}
 | 
					            {translate("resources.rooms.helper.forward_extremities")}
 | 
				
			||||||
          </Box>
 | 
					          </Box>
 | 
				
			||||||
          <ReferenceManyField
 | 
					          <ReferenceManyField reference="forward_extremities" target="room_id" label={false}>
 | 
				
			||||||
            reference="forward_extremities"
 | 
					 | 
				
			||||||
            target="room_id"
 | 
					 | 
				
			||||||
            addLabel={false}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
 | 
					            <Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
 | 
				
			||||||
              <TextField source="id" sortable={false} />
 | 
					              <TextField source="id" sortable={false} />
 | 
				
			||||||
              <DateField
 | 
					              <DateField source="received_ts" showTime options={DATE_FORMAT} sortable={false} />
 | 
				
			||||||
                source="received_ts"
 | 
					 | 
				
			||||||
                showTime
 | 
					 | 
				
			||||||
                options={date_format}
 | 
					 | 
				
			||||||
                sortable={false}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <NumberField source="depth" sortable={false} />
 | 
					              <NumberField source="depth" sortable={false} />
 | 
				
			||||||
              <TextField source="state_group" sortable={false} />
 | 
					              <TextField source="state_group" sortable={false} />
 | 
				
			||||||
            </Datagrid>
 | 
					            </Datagrid>
 | 
				
			||||||
| 
						 | 
					@ -296,7 +228,7 @@ const RoomListActions = () => (
 | 
				
			||||||
  </TopToolbar>
 | 
					  </TopToolbar>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RoomList = props => {
 | 
					export const RoomList = (props: ListProps) => {
 | 
				
			||||||
  const theme = useTheme();
 | 
					  const theme = useTheme();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
| 
						 | 
					@ -310,12 +242,7 @@ export const RoomList = props => {
 | 
				
			||||||
      <DatagridConfigurable
 | 
					      <DatagridConfigurable
 | 
				
			||||||
        rowClick="show"
 | 
					        rowClick="show"
 | 
				
			||||||
        bulkActionButtons={<RoomBulkActionButtons />}
 | 
					        bulkActionButtons={<RoomBulkActionButtons />}
 | 
				
			||||||
        omit={[
 | 
					        omit={["joined_local_members", "state_events", "version", "federatable"]}
 | 
				
			||||||
          "joined_local_members",
 | 
					 | 
				
			||||||
          "state_events",
 | 
					 | 
				
			||||||
          "version",
 | 
					 | 
				
			||||||
          "federatable",
 | 
					 | 
				
			||||||
        ]}
 | 
					 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <BooleanField
 | 
					        <BooleanField
 | 
				
			||||||
          source="is_encrypted"
 | 
					          source="is_encrypted"
 | 
				
			||||||
| 
						 | 
					@ -328,12 +255,7 @@ export const RoomList = props => {
 | 
				
			||||||
            [`& [data-testid="false"]`]: { color: theme.palette.error.main },
 | 
					            [`& [data-testid="false"]`]: { color: theme.palette.error.main },
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <FunctionField
 | 
					        <FunctionField source="name" render={record => record["name"] || record["canonical_alias"] || record["id"]} />
 | 
				
			||||||
          source="name"
 | 
					 | 
				
			||||||
          render={record =>
 | 
					 | 
				
			||||||
            record["name"] || record["canonical_alias"] || record["id"]
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <TextField source="joined_members" />
 | 
					        <TextField source="joined_members" />
 | 
				
			||||||
        <TextField source="joined_local_members" />
 | 
					        <TextField source="joined_local_members" />
 | 
				
			||||||
        <TextField source="state_events" />
 | 
					        <TextField source="state_events" />
 | 
				
			||||||
| 
						 | 
					@ -345,7 +267,7 @@ export const RoomList = props => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const resource = {
 | 
					const resource: ResourceProps = {
 | 
				
			||||||
  name: "rooms",
 | 
					  name: "rooms",
 | 
				
			||||||
  icon: RoomIcon,
 | 
					  icon: RoomIcon,
 | 
				
			||||||
  list: RoomList,
 | 
					  list: RoomList,
 | 
				
			||||||
| 
						 | 
					@ -1,79 +0,0 @@
 | 
				
			||||||
import React from "react";
 | 
					 | 
				
			||||||
import { cloneElement } from "react";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  Datagrid,
 | 
					 | 
				
			||||||
  ExportButton,
 | 
					 | 
				
			||||||
  List,
 | 
					 | 
				
			||||||
  NumberField,
 | 
					 | 
				
			||||||
  Pagination,
 | 
					 | 
				
			||||||
  sanitizeListRestProps,
 | 
					 | 
				
			||||||
  SearchInput,
 | 
					 | 
				
			||||||
  TextField,
 | 
					 | 
				
			||||||
  TopToolbar,
 | 
					 | 
				
			||||||
  useListContext,
 | 
					 | 
				
			||||||
} from "react-admin";
 | 
					 | 
				
			||||||
import EqualizerIcon from "@mui/icons-material/Equalizer";
 | 
					 | 
				
			||||||
import { DeleteMediaButton } from "./media";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const ListActions = props => {
 | 
					 | 
				
			||||||
  const { className, exporter, filters, maxResults, ...rest } = props;
 | 
					 | 
				
			||||||
  const { sort, resource, displayedFilters, filterValues, showFilter, total } =
 | 
					 | 
				
			||||||
    useListContext();
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <TopToolbar className={className} {...sanitizeListRestProps(rest)}>
 | 
					 | 
				
			||||||
      {filters &&
 | 
					 | 
				
			||||||
        cloneElement(filters, {
 | 
					 | 
				
			||||||
          resource,
 | 
					 | 
				
			||||||
          showFilter,
 | 
					 | 
				
			||||||
          displayedFilters,
 | 
					 | 
				
			||||||
          filterValues,
 | 
					 | 
				
			||||||
          context: "button",
 | 
					 | 
				
			||||||
        })}
 | 
					 | 
				
			||||||
      <DeleteMediaButton />
 | 
					 | 
				
			||||||
      <ExportButton
 | 
					 | 
				
			||||||
        disabled={total === 0}
 | 
					 | 
				
			||||||
        resource={resource}
 | 
					 | 
				
			||||||
        sort={sort}
 | 
					 | 
				
			||||||
        filterValues={filterValues}
 | 
					 | 
				
			||||||
        maxResults={maxResults}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </TopToolbar>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const UserMediaStatsPagination = () => (
 | 
					 | 
				
			||||||
  <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const userMediaStatsFilters = [<SearchInput source="search_term" alwaysOn />];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const UserMediaStatsList = props => (
 | 
					 | 
				
			||||||
  <List
 | 
					 | 
				
			||||||
    {...props}
 | 
					 | 
				
			||||||
    actions={<ListActions />}
 | 
					 | 
				
			||||||
    filters={userMediaStatsFilters}
 | 
					 | 
				
			||||||
    pagination={<UserMediaStatsPagination />}
 | 
					 | 
				
			||||||
    sort={{ field: "media_length", order: "DESC" }}
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <Datagrid
 | 
					 | 
				
			||||||
      rowClick={(id, resource, record) => "/users/" + id + "/media"}
 | 
					 | 
				
			||||||
      bulkActionButtons={false}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <TextField source="user_id" label="resources.users.fields.id" />
 | 
					 | 
				
			||||||
      <TextField
 | 
					 | 
				
			||||||
        source="displayname"
 | 
					 | 
				
			||||||
        label="resources.users.fields.displayname"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <NumberField source="media_count" />
 | 
					 | 
				
			||||||
      <NumberField source="media_length" />
 | 
					 | 
				
			||||||
    </Datagrid>
 | 
					 | 
				
			||||||
  </List>
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const resource = {
 | 
					 | 
				
			||||||
  name: "user_media_statistics",
 | 
					 | 
				
			||||||
  icon: EqualizerIcon,
 | 
					 | 
				
			||||||
  list: UserMediaStatsList,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default resource;
 | 
					 | 
				
			||||||
							
								
								
									
										55
									
								
								src/components/statistics.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/components/statistics.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,55 @@
 | 
				
			||||||
 | 
					import EqualizerIcon from "@mui/icons-material/Equalizer";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Datagrid,
 | 
				
			||||||
 | 
					  ExportButton,
 | 
				
			||||||
 | 
					  List,
 | 
				
			||||||
 | 
					  ListProps,
 | 
				
			||||||
 | 
					  NumberField,
 | 
				
			||||||
 | 
					  Pagination,
 | 
				
			||||||
 | 
					  ResourceProps,
 | 
				
			||||||
 | 
					  SearchInput,
 | 
				
			||||||
 | 
					  TextField,
 | 
				
			||||||
 | 
					  TopToolbar,
 | 
				
			||||||
 | 
					  useListContext,
 | 
				
			||||||
 | 
					} from "react-admin";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { DeleteMediaButton } from "./media";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ListActions = () => {
 | 
				
			||||||
 | 
					  const { isLoading, total } = useListContext();
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <TopToolbar>
 | 
				
			||||||
 | 
					      <DeleteMediaButton />
 | 
				
			||||||
 | 
					      <ExportButton disabled={isLoading || total === 0} />
 | 
				
			||||||
 | 
					    </TopToolbar>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const UserMediaStatsPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const userMediaStatsFilters = [<SearchInput source="search_term" alwaysOn />];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const UserMediaStatsList = (props: ListProps) => (
 | 
				
			||||||
 | 
					  <List
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					    actions={<ListActions />}
 | 
				
			||||||
 | 
					    filters={userMediaStatsFilters}
 | 
				
			||||||
 | 
					    pagination={<UserMediaStatsPagination />}
 | 
				
			||||||
 | 
					    sort={{ field: "media_length", order: "DESC" }}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <Datagrid rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}>
 | 
				
			||||||
 | 
					      <TextField source="user_id" label="resources.users.fields.id" />
 | 
				
			||||||
 | 
					      <TextField source="displayname" label="resources.users.fields.displayname" />
 | 
				
			||||||
 | 
					      <NumberField source="media_count" />
 | 
				
			||||||
 | 
					      <NumberField source="media_length" />
 | 
				
			||||||
 | 
					    </Datagrid>
 | 
				
			||||||
 | 
					  </List>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const resource: ResourceProps = {
 | 
				
			||||||
 | 
					  name: "user_media_statistics",
 | 
				
			||||||
 | 
					  icon: EqualizerIcon,
 | 
				
			||||||
 | 
					  list: UserMediaStatsList,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default resource;
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,12 @@
 | 
				
			||||||
import React, { cloneElement } from "react";
 | 
					 | 
				
			||||||
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
 | 
					import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
 | 
				
			||||||
import ContactMailIcon from "@mui/icons-material/ContactMail";
 | 
					import ContactMailIcon from "@mui/icons-material/ContactMail";
 | 
				
			||||||
import DevicesIcon from "@mui/icons-material/Devices";
 | 
					import DevicesIcon from "@mui/icons-material/Devices";
 | 
				
			||||||
import GetAppIcon from "@mui/icons-material/GetApp";
 | 
					import GetAppIcon from "@mui/icons-material/GetApp";
 | 
				
			||||||
 | 
					import UserIcon from "@mui/icons-material/Group";
 | 
				
			||||||
import NotificationsIcon from "@mui/icons-material/Notifications";
 | 
					import NotificationsIcon from "@mui/icons-material/Notifications";
 | 
				
			||||||
import PermMediaIcon from "@mui/icons-material/PermMedia";
 | 
					import PermMediaIcon from "@mui/icons-material/PermMedia";
 | 
				
			||||||
import PersonPinIcon from "@mui/icons-material/PersonPin";
 | 
					import PersonPinIcon from "@mui/icons-material/PersonPin";
 | 
				
			||||||
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
 | 
					import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
 | 
				
			||||||
import UserIcon from "@mui/icons-material/Group";
 | 
					 | 
				
			||||||
import ViewListIcon from "@mui/icons-material/ViewList";
 | 
					import ViewListIcon from "@mui/icons-material/ViewList";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ArrayInput,
 | 
					  ArrayInput,
 | 
				
			||||||
| 
						 | 
					@ -16,9 +15,11 @@ import {
 | 
				
			||||||
  Datagrid,
 | 
					  Datagrid,
 | 
				
			||||||
  DateField,
 | 
					  DateField,
 | 
				
			||||||
  Create,
 | 
					  Create,
 | 
				
			||||||
 | 
					  CreateProps,
 | 
				
			||||||
  Edit,
 | 
					  Edit,
 | 
				
			||||||
 | 
					  EditProps,
 | 
				
			||||||
  List,
 | 
					  List,
 | 
				
			||||||
  Toolbar,
 | 
					  ListProps,
 | 
				
			||||||
  SimpleForm,
 | 
					  SimpleForm,
 | 
				
			||||||
  SimpleFormIterator,
 | 
					  SimpleFormIterator,
 | 
				
			||||||
  TabbedForm,
 | 
					  TabbedForm,
 | 
				
			||||||
| 
						 | 
					@ -30,11 +31,11 @@ import {
 | 
				
			||||||
  TextInput,
 | 
					  TextInput,
 | 
				
			||||||
  ReferenceField,
 | 
					  ReferenceField,
 | 
				
			||||||
  ReferenceManyField,
 | 
					  ReferenceManyField,
 | 
				
			||||||
 | 
					  ResourceProps,
 | 
				
			||||||
  SearchInput,
 | 
					  SearchInput,
 | 
				
			||||||
  SelectInput,
 | 
					  SelectInput,
 | 
				
			||||||
  BulkDeleteButton,
 | 
					  BulkDeleteButton,
 | 
				
			||||||
  DeleteButton,
 | 
					  DeleteButton,
 | 
				
			||||||
  SaveButton,
 | 
					 | 
				
			||||||
  maxLength,
 | 
					  maxLength,
 | 
				
			||||||
  regex,
 | 
					  regex,
 | 
				
			||||||
  required,
 | 
					  required,
 | 
				
			||||||
| 
						 | 
					@ -44,18 +45,16 @@ import {
 | 
				
			||||||
  CreateButton,
 | 
					  CreateButton,
 | 
				
			||||||
  ExportButton,
 | 
					  ExportButton,
 | 
				
			||||||
  TopToolbar,
 | 
					  TopToolbar,
 | 
				
			||||||
  sanitizeListRestProps,
 | 
					 | 
				
			||||||
  NumberField,
 | 
					  NumberField,
 | 
				
			||||||
 | 
					  useListContext,
 | 
				
			||||||
} from "react-admin";
 | 
					} from "react-admin";
 | 
				
			||||||
import { Link } from "react-router-dom";
 | 
					import { Link } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import AvatarField from "./AvatarField";
 | 
					import AvatarField from "./AvatarField";
 | 
				
			||||||
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
 | 
					import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
 | 
				
			||||||
 | 
					import { DATE_FORMAT } from "./date";
 | 
				
			||||||
import { DeviceRemoveButton } from "./devices";
 | 
					import { DeviceRemoveButton } from "./devices";
 | 
				
			||||||
import {
 | 
					import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "./media";
 | 
				
			||||||
  MediaIDField,
 | 
					 | 
				
			||||||
  ProtectMediaButton,
 | 
					 | 
				
			||||||
  QuarantineMediaButton,
 | 
					 | 
				
			||||||
} from "./media";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const choices_medium = [
 | 
					const choices_medium = [
 | 
				
			||||||
  { id: "email", name: "resources.users.email" },
 | 
					  { id: "email", name: "resources.users.email" },
 | 
				
			||||||
| 
						 | 
					@ -67,52 +66,12 @@ const choices_type = [
 | 
				
			||||||
  { id: "support", name: "support" },
 | 
					  { id: "support", name: "support" },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const date_format = {
 | 
					const UserListActions = () => {
 | 
				
			||||||
  year: "numeric",
 | 
					  const { isLoading, total } = useListContext();
 | 
				
			||||||
  month: "2-digit",
 | 
					 | 
				
			||||||
  day: "2-digit",
 | 
					 | 
				
			||||||
  hour: "2-digit",
 | 
					 | 
				
			||||||
  minute: "2-digit",
 | 
					 | 
				
			||||||
  second: "2-digit",
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const UserListActions = ({
 | 
					 | 
				
			||||||
  sort,
 | 
					 | 
				
			||||||
  className,
 | 
					 | 
				
			||||||
  resource,
 | 
					 | 
				
			||||||
  filters,
 | 
					 | 
				
			||||||
  displayedFilters,
 | 
					 | 
				
			||||||
  exporter, // you can hide ExportButton if exporter = (null || false)
 | 
					 | 
				
			||||||
  filterValues,
 | 
					 | 
				
			||||||
  permanentFilter,
 | 
					 | 
				
			||||||
  hasCreate, // you can hide CreateButton if hasCreate = false
 | 
					 | 
				
			||||||
  selectedIds,
 | 
					 | 
				
			||||||
  onUnselectItems,
 | 
					 | 
				
			||||||
  showFilter,
 | 
					 | 
				
			||||||
  maxResults,
 | 
					 | 
				
			||||||
  total,
 | 
					 | 
				
			||||||
  ...rest
 | 
					 | 
				
			||||||
}) => {
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <TopToolbar className={className} {...sanitizeListRestProps(rest)}>
 | 
					    <TopToolbar>
 | 
				
			||||||
      {filters &&
 | 
					 | 
				
			||||||
        cloneElement(filters, {
 | 
					 | 
				
			||||||
          resource,
 | 
					 | 
				
			||||||
          showFilter,
 | 
					 | 
				
			||||||
          displayedFilters,
 | 
					 | 
				
			||||||
          filterValues,
 | 
					 | 
				
			||||||
          context: "button",
 | 
					 | 
				
			||||||
        })}
 | 
					 | 
				
			||||||
      <CreateButton />
 | 
					      <CreateButton />
 | 
				
			||||||
      <ExportButton
 | 
					      <ExportButton disabled={isLoading || total === 0} maxResults={10000} />
 | 
				
			||||||
        disabled={total === 0}
 | 
					 | 
				
			||||||
        resource={resource}
 | 
					 | 
				
			||||||
        sort={sort}
 | 
					 | 
				
			||||||
        filter={{ ...filterValues, ...permanentFilter }}
 | 
					 | 
				
			||||||
        exporter={exporter}
 | 
					 | 
				
			||||||
        maxResults={maxResults}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      {/* Add your custom actions */}
 | 
					 | 
				
			||||||
      <Button component={Link} to="/import_users" label="CSV Import">
 | 
					      <Button component={Link} to="/import_users" label="CSV Import">
 | 
				
			||||||
        <GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
 | 
					        <GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
 | 
				
			||||||
      </Button>
 | 
					      </Button>
 | 
				
			||||||
| 
						 | 
					@ -125,18 +84,12 @@ UserListActions.defaultProps = {
 | 
				
			||||||
  onUnselectItems: () => null,
 | 
					  onUnselectItems: () => null,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const UserPagination = () => (
 | 
					const UserPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
 | 
				
			||||||
  <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const userFilters = [
 | 
					const userFilters = [
 | 
				
			||||||
  <SearchInput source="name" alwaysOn />,
 | 
					  <SearchInput source="name" alwaysOn />,
 | 
				
			||||||
  <BooleanInput source="guests" alwaysOn />,
 | 
					  <BooleanInput source="guests" alwaysOn />,
 | 
				
			||||||
  <BooleanInput
 | 
					  <BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />,
 | 
				
			||||||
    label="resources.users.fields.show_deactivated"
 | 
					 | 
				
			||||||
    source="deactivated"
 | 
					 | 
				
			||||||
    alwaysOn
 | 
					 | 
				
			||||||
  />,
 | 
					 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const UserBulkActionButtons = () => (
 | 
					const UserBulkActionButtons = () => (
 | 
				
			||||||
| 
						 | 
					@ -150,32 +103,25 @@ const UserBulkActionButtons = () => (
 | 
				
			||||||
  </>
 | 
					  </>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const UserList = props => (
 | 
					export const UserList = (props: ListProps) => (
 | 
				
			||||||
  <List
 | 
					  <List
 | 
				
			||||||
    {...props}
 | 
					    {...props}
 | 
				
			||||||
    filters={userFilters}
 | 
					    filters={userFilters}
 | 
				
			||||||
    filterDefaultValues={{ guests: true, deactivated: false }}
 | 
					    filterDefaultValues={{ guests: true, deactivated: false }}
 | 
				
			||||||
    sort={{ field: "name", order: "ASC" }}
 | 
					    sort={{ field: "name", order: "ASC" }}
 | 
				
			||||||
    actions={<UserListActions maxResults={10000} />}
 | 
					    actions={<UserListActions />}
 | 
				
			||||||
    pagination={<UserPagination />}
 | 
					    pagination={<UserPagination />}
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
 | 
					    <Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
 | 
				
			||||||
      <AvatarField
 | 
					      <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" />
 | 
				
			||||||
        source="avatar_src"
 | 
					 | 
				
			||||||
        sx={{ height: "40px", width: "40px" }}
 | 
					 | 
				
			||||||
        sortBy="avatar_url"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <TextField source="id" sortBy="name" />
 | 
					      <TextField source="id" sortBy="name" />
 | 
				
			||||||
      <TextField source="displayname" />
 | 
					      <TextField source="displayname" />
 | 
				
			||||||
      <BooleanField source="is_guest" />
 | 
					      <BooleanField source="is_guest" />
 | 
				
			||||||
      <BooleanField source="admin" />
 | 
					      <BooleanField source="admin" />
 | 
				
			||||||
      <BooleanField source="deactivated" />
 | 
					      <BooleanField source="deactivated" />
 | 
				
			||||||
      <DateField
 | 
					      <BooleanField source="locked" />
 | 
				
			||||||
        source="creation_ts"
 | 
					      <BooleanField source="erased" sortable={false} />
 | 
				
			||||||
        label="resources.users.fields.creation_ts_ms"
 | 
					      <DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
 | 
				
			||||||
        showTime
 | 
					 | 
				
			||||||
        options={date_format}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </Datagrid>
 | 
					    </Datagrid>
 | 
				
			||||||
  </List>
 | 
					  </List>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
| 
						 | 
					@ -184,73 +130,18 @@ export const UserList = props => (
 | 
				
			||||||
// here only local part of user_id
 | 
					// here only local part of user_id
 | 
				
			||||||
// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
 | 
					// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
 | 
				
			||||||
// localStorage.getItem("home_server").length is not valid here
 | 
					// localStorage.getItem("home_server").length is not valid here
 | 
				
			||||||
const validateUser = [
 | 
					const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id")];
 | 
				
			||||||
  required(),
 | 
					 | 
				
			||||||
  maxLength(253),
 | 
					 | 
				
			||||||
  regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id"),
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const validateAddress = [required(), maxLength(255)];
 | 
					const validateAddress = [required(), maxLength(255)];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function generateRandomUser() {
 | 
					const UserEditActions = () => {
 | 
				
			||||||
  const homeserver = localStorage.getItem("home_server");
 | 
					  const record = useRecordContext();
 | 
				
			||||||
  const user_id =
 | 
					 | 
				
			||||||
    "@" +
 | 
					 | 
				
			||||||
    Array(8)
 | 
					 | 
				
			||||||
      .fill("0123456789abcdefghijklmnopqrstuvwxyz")
 | 
					 | 
				
			||||||
      .map(
 | 
					 | 
				
			||||||
        x =>
 | 
					 | 
				
			||||||
          x[
 | 
					 | 
				
			||||||
            Math.floor(
 | 
					 | 
				
			||||||
              (crypto.getRandomValues(new Uint32Array(1))[0] /
 | 
					 | 
				
			||||||
                (0xffffffff + 1)) *
 | 
					 | 
				
			||||||
                x.length
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
          ]
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
      .join("") +
 | 
					 | 
				
			||||||
    ":" +
 | 
					 | 
				
			||||||
    homeserver;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const password = Array(20)
 | 
					 | 
				
			||||||
    .fill(
 | 
					 | 
				
			||||||
      "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$"
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .map(
 | 
					 | 
				
			||||||
      x =>
 | 
					 | 
				
			||||||
        x[
 | 
					 | 
				
			||||||
          Math.floor(
 | 
					 | 
				
			||||||
            (crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)) *
 | 
					 | 
				
			||||||
              x.length
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .join("");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    id: user_id,
 | 
					 | 
				
			||||||
    password: password,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const UserEditToolbar = props => (
 | 
					 | 
				
			||||||
  <Toolbar {...props}>
 | 
					 | 
				
			||||||
    <SaveButton disabled={props.pristine} />
 | 
					 | 
				
			||||||
  </Toolbar>
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const UserEditActions = ({ data }) => {
 | 
					 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
  var userStatus = "";
 | 
					 | 
				
			||||||
  if (data) {
 | 
					 | 
				
			||||||
    userStatus = data.deactivated;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <TopToolbar>
 | 
					    <TopToolbar>
 | 
				
			||||||
      {!userStatus && <ServerNoticeButton record={data} />}
 | 
					      {!record.deactivated && <ServerNoticeButton />}
 | 
				
			||||||
      <DeleteButton
 | 
					      <DeleteButton
 | 
				
			||||||
        record={data}
 | 
					 | 
				
			||||||
        label="resources.users.action.erase"
 | 
					        label="resources.users.action.erase"
 | 
				
			||||||
        confirmTitle={translate("resources.users.helper.erase", {
 | 
					        confirmTitle={translate("resources.users.helper.erase", {
 | 
				
			||||||
          smart_count: 1,
 | 
					          smart_count: 1,
 | 
				
			||||||
| 
						 | 
					@ -261,41 +152,24 @@ const UserEditActions = ({ data }) => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const UserCreate = props => (
 | 
					export const UserCreate = (props: CreateProps) => (
 | 
				
			||||||
  <Create {...props}>
 | 
					  <Create {...props}>
 | 
				
			||||||
    <SimpleForm>
 | 
					    <SimpleForm>
 | 
				
			||||||
      <TextInput source="id" autoComplete="off" validate={validateUser} />
 | 
					      <TextInput source="id" autoComplete="off" validate={validateUser} />
 | 
				
			||||||
      <TextInput source="displayname" validate={maxLength(256)} />
 | 
					      <TextInput source="displayname" validate={maxLength(256)} />
 | 
				
			||||||
      <PasswordInput
 | 
					      <PasswordInput source="password" autoComplete="new-password" validate={maxLength(512)} />
 | 
				
			||||||
        source="password"
 | 
					      <SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
 | 
				
			||||||
        autoComplete="new-password"
 | 
					 | 
				
			||||||
        validate={maxLength(512)}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <SelectInput
 | 
					 | 
				
			||||||
        source="user_type"
 | 
					 | 
				
			||||||
        choices={choices_type}
 | 
					 | 
				
			||||||
        translateChoice={false}
 | 
					 | 
				
			||||||
        resettable
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <BooleanInput source="admin" />
 | 
					      <BooleanInput source="admin" />
 | 
				
			||||||
      <ArrayInput source="threepids">
 | 
					      <ArrayInput source="threepids">
 | 
				
			||||||
        <SimpleFormIterator disableReordering>
 | 
					        <SimpleFormIterator disableReordering>
 | 
				
			||||||
          <SelectInput
 | 
					          <SelectInput source="medium" choices={choices_medium} validate={required()} />
 | 
				
			||||||
            source="medium"
 | 
					 | 
				
			||||||
            choices={choices_medium}
 | 
					 | 
				
			||||||
            validate={required()}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <TextInput source="address" validate={validateAddress} />
 | 
					          <TextInput source="address" validate={validateAddress} />
 | 
				
			||||||
        </SimpleFormIterator>
 | 
					        </SimpleFormIterator>
 | 
				
			||||||
      </ArrayInput>
 | 
					      </ArrayInput>
 | 
				
			||||||
      <ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
 | 
					      <ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
 | 
				
			||||||
        <SimpleFormIterator disableReordering>
 | 
					        <SimpleFormIterator disableReordering>
 | 
				
			||||||
          <TextInput source="auth_provider" validate={required()} />
 | 
					          <TextInput source="auth_provider" validate={required()} />
 | 
				
			||||||
          <TextInput
 | 
					          <TextInput source="external_id" label="resources.users.fields.id" validate={required()} />
 | 
				
			||||||
            source="external_id"
 | 
					 | 
				
			||||||
            label="resources.users.fields.id"
 | 
					 | 
				
			||||||
            validate={required()}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </SimpleFormIterator>
 | 
					        </SimpleFormIterator>
 | 
				
			||||||
      </ArrayInput>
 | 
					      </ArrayInput>
 | 
				
			||||||
    </SimpleForm>
 | 
					    </SimpleForm>
 | 
				
			||||||
| 
						 | 
					@ -315,47 +189,26 @@ const UserTitle = () => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const UserEdit = props => {
 | 
					export const UserEdit = (props: EditProps) => {
 | 
				
			||||||
  const translate = useTranslate();
 | 
					  const translate = useTranslate();
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
 | 
					    <Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
 | 
				
			||||||
      <TabbedForm toolbar={<UserEditToolbar />}>
 | 
					      <TabbedForm>
 | 
				
			||||||
        <FormTab
 | 
					        <FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
 | 
				
			||||||
          label={translate("resources.users.name", { smart_count: 1 })}
 | 
					          <AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} />
 | 
				
			||||||
          icon={<PersonPinIcon />}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <AvatarField
 | 
					 | 
				
			||||||
            source="avatar_src"
 | 
					 | 
				
			||||||
            sortable={false}
 | 
					 | 
				
			||||||
            sx={{ height: "120px", width: "120px", float: "right" }}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <TextInput source="id" disabled />
 | 
					          <TextInput source="id" disabled />
 | 
				
			||||||
          <TextInput source="displayname" />
 | 
					          <TextInput source="displayname" />
 | 
				
			||||||
          <PasswordInput
 | 
					          <PasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
 | 
				
			||||||
            source="password"
 | 
					          <SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
 | 
				
			||||||
            autoComplete="new-password"
 | 
					 | 
				
			||||||
            helperText="resources.users.helper.password"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <SelectInput
 | 
					 | 
				
			||||||
            source="user_type"
 | 
					 | 
				
			||||||
            choices={choices_type}
 | 
					 | 
				
			||||||
            translateChoice={false}
 | 
					 | 
				
			||||||
            resettable
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <BooleanInput source="admin" />
 | 
					          <BooleanInput source="admin" />
 | 
				
			||||||
          <BooleanInput
 | 
					          <BooleanInput source="locked" />
 | 
				
			||||||
            source="deactivated"
 | 
					          <BooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
 | 
				
			||||||
            helperText="resources.users.helper.deactivate"
 | 
					          <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" />
 | 
				
			||||||
        </FormTab>
 | 
					        </FormTab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <FormTab
 | 
					        <FormTab label="resources.users.threepid" icon={<ContactMailIcon />} path="threepid">
 | 
				
			||||||
          label="resources.users.threepid"
 | 
					 | 
				
			||||||
          icon={<ContactMailIcon />}
 | 
					 | 
				
			||||||
          path="threepid"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <ArrayInput source="threepids">
 | 
					          <ArrayInput source="threepids">
 | 
				
			||||||
            <SimpleFormIterator disableReordering>
 | 
					            <SimpleFormIterator disableReordering>
 | 
				
			||||||
              <SelectInput source="medium" choices={choices_medium} />
 | 
					              <SelectInput source="medium" choices={choices_medium} />
 | 
				
			||||||
| 
						 | 
					@ -364,76 +217,34 @@ export const UserEdit = props => {
 | 
				
			||||||
          </ArrayInput>
 | 
					          </ArrayInput>
 | 
				
			||||||
        </FormTab>
 | 
					        </FormTab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <FormTab
 | 
					        <FormTab label="synapseadmin.users.tabs.sso" icon={<AssignmentIndIcon />} path="sso">
 | 
				
			||||||
          label="synapseadmin.users.tabs.sso"
 | 
					 | 
				
			||||||
          icon={<AssignmentIndIcon />}
 | 
					 | 
				
			||||||
          path="sso"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <ArrayInput source="external_ids" label={false}>
 | 
					          <ArrayInput source="external_ids" label={false}>
 | 
				
			||||||
            <SimpleFormIterator disableReordering>
 | 
					            <SimpleFormIterator disableReordering>
 | 
				
			||||||
              <TextInput source="auth_provider" validate={required()} />
 | 
					              <TextInput source="auth_provider" validate={required()} />
 | 
				
			||||||
              <TextInput
 | 
					              <TextInput source="external_id" label="resources.users.fields.id" validate={required()} />
 | 
				
			||||||
                source="external_id"
 | 
					 | 
				
			||||||
                label="resources.users.fields.id"
 | 
					 | 
				
			||||||
                validate={required()}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </SimpleFormIterator>
 | 
					            </SimpleFormIterator>
 | 
				
			||||||
          </ArrayInput>
 | 
					          </ArrayInput>
 | 
				
			||||||
        </FormTab>
 | 
					        </FormTab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <FormTab
 | 
					        <FormTab label={translate("resources.devices.name", { smart_count: 2 })} icon={<DevicesIcon />} path="devices">
 | 
				
			||||||
          label={translate("resources.devices.name", { smart_count: 2 })}
 | 
					          <ReferenceManyField reference="devices" target="user_id" label={false}>
 | 
				
			||||||
          icon={<DevicesIcon />}
 | 
					 | 
				
			||||||
          path="devices"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <ReferenceManyField
 | 
					 | 
				
			||||||
            reference="devices"
 | 
					 | 
				
			||||||
            target="user_id"
 | 
					 | 
				
			||||||
            addLabel={false}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Datagrid style={{ width: "100%" }}>
 | 
					            <Datagrid style={{ width: "100%" }}>
 | 
				
			||||||
              <TextField source="device_id" sortable={false} />
 | 
					              <TextField source="device_id" sortable={false} />
 | 
				
			||||||
              <TextField source="display_name" sortable={false} />
 | 
					              <TextField source="display_name" sortable={false} />
 | 
				
			||||||
              <TextField source="last_seen_ip" sortable={false} />
 | 
					              <TextField source="last_seen_ip" sortable={false} />
 | 
				
			||||||
              <DateField
 | 
					              <DateField source="last_seen_ts" showTime options={DATE_FORMAT} sortable={false} />
 | 
				
			||||||
                source="last_seen_ts"
 | 
					 | 
				
			||||||
                showTime
 | 
					 | 
				
			||||||
                options={date_format}
 | 
					 | 
				
			||||||
                sortable={false}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <DeviceRemoveButton />
 | 
					              <DeviceRemoveButton />
 | 
				
			||||||
            </Datagrid>
 | 
					            </Datagrid>
 | 
				
			||||||
          </ReferenceManyField>
 | 
					          </ReferenceManyField>
 | 
				
			||||||
        </FormTab>
 | 
					        </FormTab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <FormTab
 | 
					        <FormTab label="resources.connections.name" icon={<SettingsInputComponentIcon />} path="connections">
 | 
				
			||||||
          label="resources.connections.name"
 | 
					          <ReferenceField reference="connections" source="id" label={false} link={false}>
 | 
				
			||||||
          icon={<SettingsInputComponentIcon />}
 | 
					            <ArrayField source="devices[].sessions[0].connections" label="resources.connections.name">
 | 
				
			||||||
          path="connections"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <ReferenceField
 | 
					 | 
				
			||||||
            reference="connections"
 | 
					 | 
				
			||||||
            source="id"
 | 
					 | 
				
			||||||
            addLabel={false}
 | 
					 | 
				
			||||||
            link={false}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <ArrayField
 | 
					 | 
				
			||||||
              source="devices[].sessions[0].connections"
 | 
					 | 
				
			||||||
              label="resources.connections.name"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
 | 
					              <Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
 | 
				
			||||||
                <TextField source="ip" sortable={false} />
 | 
					                <TextField source="ip" sortable={false} />
 | 
				
			||||||
                <DateField
 | 
					                <DateField source="last_seen" showTime options={DATE_FORMAT} sortable={false} />
 | 
				
			||||||
                  source="last_seen"
 | 
					                <TextField source="user_agent" sortable={false} style={{ width: "100%" }} />
 | 
				
			||||||
                  showTime
 | 
					 | 
				
			||||||
                  options={date_format}
 | 
					 | 
				
			||||||
                  sortable={false}
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
                <TextField
 | 
					 | 
				
			||||||
                  source="user_agent"
 | 
					 | 
				
			||||||
                  sortable={false}
 | 
					 | 
				
			||||||
                  style={{ width: "100%" }}
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              </Datagrid>
 | 
					              </Datagrid>
 | 
				
			||||||
            </ArrayField>
 | 
					            </ArrayField>
 | 
				
			||||||
          </ReferenceField>
 | 
					          </ReferenceField>
 | 
				
			||||||
| 
						 | 
					@ -447,19 +258,15 @@ export const UserEdit = props => {
 | 
				
			||||||
          <ReferenceManyField
 | 
					          <ReferenceManyField
 | 
				
			||||||
            reference="users_media"
 | 
					            reference="users_media"
 | 
				
			||||||
            target="user_id"
 | 
					            target="user_id"
 | 
				
			||||||
            addLabel={false}
 | 
					            label={false}
 | 
				
			||||||
            pagination={<UserPagination />}
 | 
					            pagination={<UserPagination />}
 | 
				
			||||||
            perPage={50}
 | 
					            perPage={50}
 | 
				
			||||||
            sort={{ field: "created_ts", order: "DESC" }}
 | 
					            sort={{ field: "created_ts", order: "DESC" }}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <Datagrid style={{ width: "100%" }}>
 | 
					            <Datagrid style={{ width: "100%" }}>
 | 
				
			||||||
              <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
 | 
					              <DateField source="last_access_ts" showTime options={DATE_FORMAT} />
 | 
				
			||||||
                source="last_access_ts"
 | 
					 | 
				
			||||||
                showTime
 | 
					 | 
				
			||||||
                options={date_format}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <NumberField source="media_length" />
 | 
					              <NumberField source="media_length" />
 | 
				
			||||||
              <TextField source="media_type" />
 | 
					              <TextField source="media_type" />
 | 
				
			||||||
              <TextField source="upload_name" />
 | 
					              <TextField source="upload_name" />
 | 
				
			||||||
| 
						 | 
					@ -471,26 +278,10 @@ export const UserEdit = props => {
 | 
				
			||||||
          </ReferenceManyField>
 | 
					          </ReferenceManyField>
 | 
				
			||||||
        </FormTab>
 | 
					        </FormTab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <FormTab
 | 
					        <FormTab label={translate("resources.rooms.name", { smart_count: 2 })} icon={<ViewListIcon />} path="rooms">
 | 
				
			||||||
          label={translate("resources.rooms.name", { smart_count: 2 })}
 | 
					          <ReferenceManyField reference="joined_rooms" target="user_id" label={false}>
 | 
				
			||||||
          icon={<ViewListIcon />}
 | 
					            <Datagrid style={{ width: "100%" }} rowClick={id => "/rooms/" + id + "/show"} bulkActionButtons={false}>
 | 
				
			||||||
          path="rooms"
 | 
					              <TextField source="id" sortable={false} label="resources.rooms.fields.room_id" />
 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <ReferenceManyField
 | 
					 | 
				
			||||||
            reference="joined_rooms"
 | 
					 | 
				
			||||||
            target="user_id"
 | 
					 | 
				
			||||||
            addLabel={false}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Datagrid
 | 
					 | 
				
			||||||
              style={{ width: "100%" }}
 | 
					 | 
				
			||||||
              rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
 | 
					 | 
				
			||||||
              bulkActionButtons={false}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <TextField
 | 
					 | 
				
			||||||
                source="id"
 | 
					 | 
				
			||||||
                sortable={false}
 | 
					 | 
				
			||||||
                label="resources.rooms.fields.room_id"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <ReferenceField
 | 
					              <ReferenceField
 | 
				
			||||||
                label="resources.rooms.fields.name"
 | 
					                label="resources.rooms.fields.name"
 | 
				
			||||||
                source="id"
 | 
					                source="id"
 | 
				
			||||||
| 
						 | 
					@ -509,11 +300,7 @@ export const UserEdit = props => {
 | 
				
			||||||
          icon={<NotificationsIcon />}
 | 
					          icon={<NotificationsIcon />}
 | 
				
			||||||
          path="pushers"
 | 
					          path="pushers"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <ReferenceManyField
 | 
					          <ReferenceManyField reference="pushers" target="user_id" label={false}>
 | 
				
			||||||
            reference="pushers"
 | 
					 | 
				
			||||||
            target="user_id"
 | 
					 | 
				
			||||||
            addLabel={false}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
 | 
					            <Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
 | 
				
			||||||
              <TextField source="kind" sortable={false} />
 | 
					              <TextField source="kind" sortable={false} />
 | 
				
			||||||
              <TextField source="app_display_name" sortable={false} />
 | 
					              <TextField source="app_display_name" sortable={false} />
 | 
				
			||||||
| 
						 | 
					@ -531,7 +318,7 @@ export const UserEdit = props => {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const resource = {
 | 
					const resource: ResourceProps = {
 | 
				
			||||||
  name: "users",
 | 
					  name: "users",
 | 
				
			||||||
  icon: UserIcon,
 | 
					  icon: UserIcon,
 | 
				
			||||||
  list: UserList,
 | 
					  list: UserList,
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
import { formalGermanMessages } from "@haleos/ra-language-german";
 | 
					import { formalGermanMessages } from "@haleos/ra-language-german";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const de = {
 | 
					import { SynapseTranslationMessages } from ".";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const de: SynapseTranslationMessages = {
 | 
				
			||||||
  ...formalGermanMessages,
 | 
					  ...formalGermanMessages,
 | 
				
			||||||
  synapseadmin: {
 | 
					  synapseadmin: {
 | 
				
			||||||
    auth: {
 | 
					    auth: {
 | 
				
			||||||
| 
						 | 
					@ -44,11 +46,9 @@ const de = {
 | 
				
			||||||
    cards: {
 | 
					    cards: {
 | 
				
			||||||
      importstats: {
 | 
					      importstats: {
 | 
				
			||||||
        header: "Benutzer importieren",
 | 
					        header: "Benutzer importieren",
 | 
				
			||||||
        users_total:
 | 
					        users_total: "%{smart_count} Benutzer in der CSV Datei |||| %{smart_count} Benutzer in der CSV Datei",
 | 
				
			||||||
          "%{smart_count} Benutzer in der CSV Datei |||| %{smart_count} Benutzer in der CSV Datei",
 | 
					 | 
				
			||||||
        guest_count: "%{smart_count} Gast |||| %{smart_count} Gäste",
 | 
					        guest_count: "%{smart_count} Gast |||| %{smart_count} Gäste",
 | 
				
			||||||
        admin_count:
 | 
					        admin_count: "%{smart_count} Server Administrator |||| %{smart_count} Server Administratoren",
 | 
				
			||||||
          "%{smart_count} Server Administrator |||| %{smart_count} Server Administratoren",
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      conflicts: {
 | 
					      conflicts: {
 | 
				
			||||||
        header: "Konfliktstrategie",
 | 
					        header: "Konfliktstrategie",
 | 
				
			||||||
| 
						 | 
					@ -60,8 +60,7 @@ const de = {
 | 
				
			||||||
      ids: {
 | 
					      ids: {
 | 
				
			||||||
        header: "IDs",
 | 
					        header: "IDs",
 | 
				
			||||||
        all_ids_present: "IDs in jedem Eintrag vorhanden",
 | 
					        all_ids_present: "IDs in jedem Eintrag vorhanden",
 | 
				
			||||||
        count_ids_present:
 | 
					        count_ids_present: "%{smart_count} Eintrag mit ID |||| %{smart_count} Einträge mit IDs",
 | 
				
			||||||
          "%{smart_count} Eintrag mit ID |||| %{smart_count} Einträge mit IDs",
 | 
					 | 
				
			||||||
        mode: {
 | 
					        mode: {
 | 
				
			||||||
          ignore: "Ignoriere IDs der CSV-Datei und erstelle neue",
 | 
					          ignore: "Ignoriere IDs der CSV-Datei und erstelle neue",
 | 
				
			||||||
          update: "Aktualisiere existierende Benutzer",
 | 
					          update: "Aktualisiere existierende Benutzer",
 | 
				
			||||||
| 
						 | 
					@ -70,8 +69,7 @@ const de = {
 | 
				
			||||||
      passwords: {
 | 
					      passwords: {
 | 
				
			||||||
        header: "Passwörter",
 | 
					        header: "Passwörter",
 | 
				
			||||||
        all_passwords_present: "Passwörter in jedem Eintrag vorhanden",
 | 
					        all_passwords_present: "Passwörter in jedem Eintrag vorhanden",
 | 
				
			||||||
        count_passwords_present:
 | 
					        count_passwords_present: "%{smart_count} Eintrag mit Passwort |||| %{smart_count} Einträge mit Passwörtern",
 | 
				
			||||||
          "%{smart_count} Eintrag mit Passwort |||| %{smart_count} Einträge mit Passwörtern",
 | 
					 | 
				
			||||||
        use_passwords: "Verwende Passwörter aus der CSV Datei",
 | 
					        use_passwords: "Verwende Passwörter aus der CSV Datei",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      upload: {
 | 
					      upload: {
 | 
				
			||||||
| 
						 | 
					@ -85,13 +83,11 @@ const de = {
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      results: {
 | 
					      results: {
 | 
				
			||||||
        header: "Ergebnis",
 | 
					        header: "Ergebnis",
 | 
				
			||||||
        total:
 | 
					        total: "%{smart_count} Eintrag insgesamt |||| %{smart_count} Einträge insgesamt",
 | 
				
			||||||
          "%{smart_count} Eintrag insgesamt |||| %{smart_count} Einträge insgesamt",
 | 
					 | 
				
			||||||
        successful: "%{smart_count} Einträge erfolgreich importiert",
 | 
					        successful: "%{smart_count} Einträge erfolgreich importiert",
 | 
				
			||||||
        skipped: "%{smart_count} Einträge übersprungen",
 | 
					        skipped: "%{smart_count} Einträge übersprungen",
 | 
				
			||||||
        download_skipped: "Übersprungene Einträge herunterladen",
 | 
					        download_skipped: "Übersprungene Einträge herunterladen",
 | 
				
			||||||
        with_error:
 | 
					        with_error: "%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
 | 
				
			||||||
          "%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
 | 
					 | 
				
			||||||
        simulated_only: "Import-Vorgang war nur simuliert",
 | 
					        simulated_only: "Import-Vorgang war nur simuliert",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -108,7 +104,9 @@ const de = {
 | 
				
			||||||
        name: "Name",
 | 
					        name: "Name",
 | 
				
			||||||
        is_guest: "Gast",
 | 
					        is_guest: "Gast",
 | 
				
			||||||
        admin: "Server Administrator",
 | 
					        admin: "Server Administrator",
 | 
				
			||||||
 | 
					        locked: "Gesperrt",
 | 
				
			||||||
        deactivated: "Deaktiviert",
 | 
					        deactivated: "Deaktiviert",
 | 
				
			||||||
 | 
					        erased: "Gelöscht",
 | 
				
			||||||
        guests: "Zeige Gäste",
 | 
					        guests: "Zeige Gäste",
 | 
				
			||||||
        show_deactivated: "Zeige deaktivierte Benutzer",
 | 
					        show_deactivated: "Zeige deaktivierte Benutzer",
 | 
				
			||||||
        user_id: "Suche Benutzer",
 | 
					        user_id: "Suche Benutzer",
 | 
				
			||||||
| 
						 | 
					@ -125,10 +123,8 @@ const de = {
 | 
				
			||||||
        user_type: "Benutzertyp",
 | 
					        user_type: "Benutzertyp",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      helper: {
 | 
					      helper: {
 | 
				
			||||||
        password:
 | 
					        password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
 | 
				
			||||||
          "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
 | 
					        deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
 | 
				
			||||||
        deactivate:
 | 
					 | 
				
			||||||
          "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
 | 
					 | 
				
			||||||
        erase: "DSGVO konformes Löschen der Benutzerdaten",
 | 
					        erase: "DSGVO konformes Löschen der Benutzerdaten",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      action: {
 | 
					      action: {
 | 
				
			||||||
| 
						 | 
					@ -211,6 +207,7 @@ const de = {
 | 
				
			||||||
            info: {
 | 
					            info: {
 | 
				
			||||||
              mimetype: "Typ",
 | 
					              mimetype: "Typ",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            url: "URL",
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
| 
						 | 
					@ -359,8 +356,7 @@ const de = {
 | 
				
			||||||
        guest_can_join: "Gastbenutzer dürfen beitreten",
 | 
					        guest_can_join: "Gastbenutzer dürfen beitreten",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      action: {
 | 
					      action: {
 | 
				
			||||||
        title:
 | 
					        title: "Raum aus Verzeichnis löschen |||| %{smart_count} Räume aus Verzeichnis löschen",
 | 
				
			||||||
          "Raum aus Verzeichnis löschen |||| %{smart_count} Räume aus Verzeichnis löschen",
 | 
					 | 
				
			||||||
        content:
 | 
					        content:
 | 
				
			||||||
          "Möchten Sie den Raum wirklich aus dem Raumverzeichnis löschen? |||| Möchten Sie die %{smart_count} Räume wirklich aus dem Raumverzeichnis löschen?",
 | 
					          "Möchten Sie den Raum wirklich aus dem Raumverzeichnis löschen? |||| Möchten Sie die %{smart_count} Räume wirklich aus dem Raumverzeichnis löschen?",
 | 
				
			||||||
        erase: "Lösche aus Verzeichnis",
 | 
					        erase: "Lösche aus Verzeichnis",
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
import englishMessages from "ra-language-english";
 | 
					import englishMessages from "ra-language-english";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const en = {
 | 
					import { SynapseTranslationMessages } from ".";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const en: SynapseTranslationMessages = {
 | 
				
			||||||
  ...englishMessages,
 | 
					  ...englishMessages,
 | 
				
			||||||
  synapseadmin: {
 | 
					  synapseadmin: {
 | 
				
			||||||
    auth: {
 | 
					    auth: {
 | 
				
			||||||
| 
						 | 
					@ -18,6 +20,7 @@ const en = {
 | 
				
			||||||
      tabs: { sso: "SSO" },
 | 
					      tabs: { sso: "SSO" },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    rooms: {
 | 
					    rooms: {
 | 
				
			||||||
 | 
					      details: "Room details",
 | 
				
			||||||
      tabs: {
 | 
					      tabs: {
 | 
				
			||||||
        basic: "Basic",
 | 
					        basic: "Basic",
 | 
				
			||||||
        members: "Members",
 | 
					        members: "Members",
 | 
				
			||||||
| 
						 | 
					@ -32,10 +35,8 @@ const en = {
 | 
				
			||||||
      at_entry: "At entry %{entry}: %{message}",
 | 
					      at_entry: "At entry %{entry}: %{message}",
 | 
				
			||||||
      error: "Error",
 | 
					      error: "Error",
 | 
				
			||||||
      required_field: "Required field '%{field}' is not present",
 | 
					      required_field: "Required field '%{field}' is not present",
 | 
				
			||||||
      invalid_value:
 | 
					      invalid_value: "Invalid value on line %{row}. '%{field}' field may only be 'true' or 'false'",
 | 
				
			||||||
        "Invalid value on line %{row}. '%{field}' field may only be 'true' or 'false'",
 | 
					      unreasonably_big: "Refused to load unreasonably big file of %{size} megabytes",
 | 
				
			||||||
      unreasonably_big:
 | 
					 | 
				
			||||||
        "Refused to load unreasonably big file of %{size} megabytes",
 | 
					 | 
				
			||||||
      already_in_progress: "An import run is already in progress",
 | 
					      already_in_progress: "An import run is already in progress",
 | 
				
			||||||
      id_exits: "ID %{id} already present",
 | 
					      id_exits: "ID %{id} already present",
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -44,8 +45,7 @@ const en = {
 | 
				
			||||||
    cards: {
 | 
					    cards: {
 | 
				
			||||||
      importstats: {
 | 
					      importstats: {
 | 
				
			||||||
        header: "Import users",
 | 
					        header: "Import users",
 | 
				
			||||||
        users_total:
 | 
					        users_total: "%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
 | 
				
			||||||
          "%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
 | 
					 | 
				
			||||||
        guest_count: "%{smart_count} guest |||| %{smart_count} guests",
 | 
					        guest_count: "%{smart_count} guest |||| %{smart_count} guests",
 | 
				
			||||||
        admin_count: "%{smart_count} admin |||| %{smart_count} admins",
 | 
					        admin_count: "%{smart_count} admin |||| %{smart_count} admins",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
| 
						 | 
					@ -59,8 +59,7 @@ const en = {
 | 
				
			||||||
      ids: {
 | 
					      ids: {
 | 
				
			||||||
        header: "IDs",
 | 
					        header: "IDs",
 | 
				
			||||||
        all_ids_present: "IDs present on every entry",
 | 
					        all_ids_present: "IDs present on every entry",
 | 
				
			||||||
        count_ids_present:
 | 
					        count_ids_present: "%{smart_count} entry with ID |||| %{smart_count} entries with IDs",
 | 
				
			||||||
          "%{smart_count} entry with ID |||| %{smart_count} entries with IDs",
 | 
					 | 
				
			||||||
        mode: {
 | 
					        mode: {
 | 
				
			||||||
          ignore: "Ignore IDs in CSV and create new ones",
 | 
					          ignore: "Ignore IDs in CSV and create new ones",
 | 
				
			||||||
          update: "Update existing records",
 | 
					          update: "Update existing records",
 | 
				
			||||||
| 
						 | 
					@ -69,8 +68,7 @@ const en = {
 | 
				
			||||||
      passwords: {
 | 
					      passwords: {
 | 
				
			||||||
        header: "Passwords",
 | 
					        header: "Passwords",
 | 
				
			||||||
        all_passwords_present: "Passwords present on every entry",
 | 
					        all_passwords_present: "Passwords present on every entry",
 | 
				
			||||||
        count_passwords_present:
 | 
					        count_passwords_present: "%{smart_count} entry with password |||| %{smart_count} entries with passwords",
 | 
				
			||||||
          "%{smart_count} entry with password |||| %{smart_count} entries with passwords",
 | 
					 | 
				
			||||||
        use_passwords: "Use passwords from CSV",
 | 
					        use_passwords: "Use passwords from CSV",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      upload: {
 | 
					      upload: {
 | 
				
			||||||
| 
						 | 
					@ -84,13 +82,11 @@ const en = {
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      results: {
 | 
					      results: {
 | 
				
			||||||
        header: "Import results",
 | 
					        header: "Import results",
 | 
				
			||||||
        total:
 | 
					        total: "%{smart_count} entry in total |||| %{smart_count} entries in total",
 | 
				
			||||||
          "%{smart_count} entry in total |||| %{smart_count} entries in total",
 | 
					 | 
				
			||||||
        successful: "%{smart_count} entries successfully imported",
 | 
					        successful: "%{smart_count} entries successfully imported",
 | 
				
			||||||
        skipped: "%{smart_count} entries skipped",
 | 
					        skipped: "%{smart_count} entries skipped",
 | 
				
			||||||
        download_skipped: "Download skipped records",
 | 
					        download_skipped: "Download skipped records",
 | 
				
			||||||
        with_error:
 | 
					        with_error: "%{smart_count} entry with errors ||| %{smart_count} entries with errors",
 | 
				
			||||||
          "%{smart_count} entry with errors ||| %{smart_count} entries with errors",
 | 
					 | 
				
			||||||
        simulated_only: "Run was only simulated",
 | 
					        simulated_only: "Run was only simulated",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -107,7 +103,9 @@ const en = {
 | 
				
			||||||
        name: "Name",
 | 
					        name: "Name",
 | 
				
			||||||
        is_guest: "Guest",
 | 
					        is_guest: "Guest",
 | 
				
			||||||
        admin: "Server Administrator",
 | 
					        admin: "Server Administrator",
 | 
				
			||||||
 | 
					        locked: "Locked",
 | 
				
			||||||
        deactivated: "Deactivated",
 | 
					        deactivated: "Deactivated",
 | 
				
			||||||
 | 
					        erased: "Erased",
 | 
				
			||||||
        guests: "Show guests",
 | 
					        guests: "Show guests",
 | 
				
			||||||
        show_deactivated: "Show deactivated users",
 | 
					        show_deactivated: "Show deactivated users",
 | 
				
			||||||
        user_id: "Search user",
 | 
					        user_id: "Search user",
 | 
				
			||||||
| 
						 | 
					@ -215,8 +213,7 @@ const en = {
 | 
				
			||||||
      action: {
 | 
					      action: {
 | 
				
			||||||
        erase: {
 | 
					        erase: {
 | 
				
			||||||
          title: "Delete reported event",
 | 
					          title: "Delete reported event",
 | 
				
			||||||
          content:
 | 
					          content: "Are you sure you want to delete the reported event? This cannot be undone.",
 | 
				
			||||||
            "Are you sure you want to delete the reported event? This cannot be undone.",
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -357,8 +354,7 @@ const en = {
 | 
				
			||||||
        guest_can_join: "guest users may join",
 | 
					        guest_can_join: "guest users may join",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      action: {
 | 
					      action: {
 | 
				
			||||||
        title:
 | 
					        title: "Delete room from directory |||| Delete %{smart_count} rooms from directory",
 | 
				
			||||||
          "Delete room from directory |||| Delete %{smart_count} rooms from directory",
 | 
					 | 
				
			||||||
        content:
 | 
					        content:
 | 
				
			||||||
          "Are you sure you want to remove this room from directory? |||| Are you sure you want to remove these %{smart_count} rooms from directory?",
 | 
					          "Are you sure you want to remove this room from directory? |||| Are you sure you want to remove these %{smart_count} rooms from directory?",
 | 
				
			||||||
        erase: "Delete from room directory",
 | 
					        erase: "Delete from room directory",
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
import farsiMessages from "ra-language-farsi";
 | 
					import farsiMessages from "ra-language-farsi";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const fa = {
 | 
					import { SynapseTranslationMessages } from ".";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const fa: SynapseTranslationMessages = {
 | 
				
			||||||
  ...farsiMessages,
 | 
					  ...farsiMessages,
 | 
				
			||||||
  synapseadmin: {
 | 
					  synapseadmin: {
 | 
				
			||||||
    auth: {
 | 
					    auth: {
 | 
				
			||||||
| 
						 | 
					@ -31,10 +33,8 @@ const fa = {
 | 
				
			||||||
      at_entry: "در هنگام ورود %{entry}: %{message}",
 | 
					      at_entry: "در هنگام ورود %{entry}: %{message}",
 | 
				
			||||||
      error: "Error",
 | 
					      error: "Error",
 | 
				
			||||||
      required_field: "فیلد الزامی '%{field}' وجود ندارد",
 | 
					      required_field: "فیلد الزامی '%{field}' وجود ندارد",
 | 
				
			||||||
      invalid_value:
 | 
					      invalid_value: "خطا در خط %{row}. '%{field}' فیلد ممکن است فقط 'درست' یا 'نادرست' باشد",
 | 
				
			||||||
        "خطا در خط %{row}. '%{field}' فیلد ممکن است فقط 'درست' یا 'نادرست' باشد",
 | 
					      unreasonably_big: "از بارگذاری فایل هایی با حجم غیر منطقی خودداری کنید %{size} مگابایت",
 | 
				
			||||||
      unreasonably_big:
 | 
					 | 
				
			||||||
        "از بارگذاری فایل هایی با حجم غیر منطقی خودداری کنید %{size} مگابایت",
 | 
					 | 
				
			||||||
      already_in_progress: "یک بارگذاری از قبل در حال انجام است",
 | 
					      already_in_progress: "یک بارگذاری از قبل در حال انجام است",
 | 
				
			||||||
      id_exits: "شناسه %{id} موجود است",
 | 
					      id_exits: "شناسه %{id} موجود است",
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -43,8 +43,7 @@ const fa = {
 | 
				
			||||||
    cards: {
 | 
					    cards: {
 | 
				
			||||||
      importstats: {
 | 
					      importstats: {
 | 
				
			||||||
        header: "وارد کردن کاربران",
 | 
					        header: "وارد کردن کاربران",
 | 
				
			||||||
        users_total:
 | 
					        users_total: "%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
 | 
				
			||||||
          "%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
 | 
					 | 
				
			||||||
        guest_count: "%{smart_count} guest |||| %{smart_count} guests",
 | 
					        guest_count: "%{smart_count} guest |||| %{smart_count} guests",
 | 
				
			||||||
        admin_count: "%{smart_count} admin |||| %{smart_count} admins",
 | 
					        admin_count: "%{smart_count} admin |||| %{smart_count} admins",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
| 
						 | 
					@ -58,8 +57,7 @@ const fa = {
 | 
				
			||||||
      ids: {
 | 
					      ids: {
 | 
				
			||||||
        header: "شناسنامه ها",
 | 
					        header: "شناسنامه ها",
 | 
				
			||||||
        all_ids_present: "شناسه های موجود در هر ورودی",
 | 
					        all_ids_present: "شناسه های موجود در هر ورودی",
 | 
				
			||||||
        count_ids_present:
 | 
					        count_ids_present: "%{smart_count} ورود با شناسه |||| %{smart_count} ورودی با شناسه",
 | 
				
			||||||
          "%{smart_count} ورود با شناسه |||| %{smart_count} ورودی با شناسه",
 | 
					 | 
				
			||||||
        mode: {
 | 
					        mode: {
 | 
				
			||||||
          ignore: "شناسه ها را در CSV نادیده بگیر و شناسه های جدید ایجاد کن",
 | 
					          ignore: "شناسه ها را در CSV نادیده بگیر و شناسه های جدید ایجاد کن",
 | 
				
			||||||
          update: "سوابق موجود را به روز کنید",
 | 
					          update: "سوابق موجود را به روز کنید",
 | 
				
			||||||
| 
						 | 
					@ -68,8 +66,7 @@ const fa = {
 | 
				
			||||||
      passwords: {
 | 
					      passwords: {
 | 
				
			||||||
        header: "رمز عبور",
 | 
					        header: "رمز عبور",
 | 
				
			||||||
        all_passwords_present: "رمزهای عبور موجود در هر ورودی",
 | 
					        all_passwords_present: "رمزهای عبور موجود در هر ورودی",
 | 
				
			||||||
        count_passwords_present:
 | 
					        count_passwords_present: "%{smart_count} ورود با رمز عبور |||| %{smart_count} ورودی با رمز عبور",
 | 
				
			||||||
          "%{smart_count} ورود با رمز عبور |||| %{smart_count} ورودی با رمز عبور",
 | 
					 | 
				
			||||||
        use_passwords: "از پسوردهای CSV استفاده کنید",
 | 
					        use_passwords: "از پسوردهای CSV استفاده کنید",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      upload: {
 | 
					      upload: {
 | 
				
			||||||
| 
						 | 
					@ -87,8 +84,7 @@ const fa = {
 | 
				
			||||||
        successful: "%{smart_count} ورودی ها با موفقیت وارد شدند",
 | 
					        successful: "%{smart_count} ورودی ها با موفقیت وارد شدند",
 | 
				
			||||||
        skipped: "%{smart_count} ورودی ها نادیده گرفته شدند",
 | 
					        skipped: "%{smart_count} ورودی ها نادیده گرفته شدند",
 | 
				
			||||||
        download_skipped: "دانلود رکوردهای نادیده گرفته شده",
 | 
					        download_skipped: "دانلود رکوردهای نادیده گرفته شده",
 | 
				
			||||||
        with_error:
 | 
					        with_error: "%{smart_count} ورود با خطا ||| %{smart_count} ورودی های دارای خطا",
 | 
				
			||||||
          "%{smart_count} ورود با خطا ||| %{smart_count} ورودی های دارای خطا",
 | 
					 | 
				
			||||||
        simulated_only: "اجرا فقط شبیه سازی شد",
 | 
					        simulated_only: "اجرا فقط شبیه سازی شد",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -226,8 +222,7 @@ const fa = {
 | 
				
			||||||
      action: {
 | 
					      action: {
 | 
				
			||||||
        erase: {
 | 
					        erase: {
 | 
				
			||||||
          title: "حذف کردن %{id}",
 | 
					          title: "حذف کردن %{id}",
 | 
				
			||||||
          content:
 | 
					          content: 'آیا مطمئن هستید که می خواهید دستگاه را حذف کنید؟ "%{name}"?',
 | 
				
			||||||
            'آیا مطمئن هستید که می خواهید دستگاه را حذف کنید؟ "%{name}"?',
 | 
					 | 
				
			||||||
          success: "دستگاه با موفقیت حذف شد.",
 | 
					          success: "دستگاه با موفقیت حذف شد.",
 | 
				
			||||||
          failure: "خطایی رخ داده است.",
 | 
					          failure: "خطایی رخ داده است.",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
| 
						 | 
					@ -342,8 +337,7 @@ const fa = {
 | 
				
			||||||
        guest_can_join: "کاربران مهمان ممکن است ملحق شوند",
 | 
					        guest_can_join: "کاربران مهمان ممکن است ملحق شوند",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      action: {
 | 
					      action: {
 | 
				
			||||||
        title:
 | 
					        title: "اتاق را از فهرست حذف کنید |||| حذف کنید %{smart_count} اتاق ها از دایرکتوری",
 | 
				
			||||||
          "اتاق را از فهرست حذف کنید |||| حذف کنید %{smart_count} اتاق ها از دایرکتوری",
 | 
					 | 
				
			||||||
        content:
 | 
					        content:
 | 
				
			||||||
          "آیا مطمئنید که می خواهید این اتاق را از فهرست راهنمای حذف کنید؟ |||| آیا مطمئن هستید که می خواهید این موارد را %{smart_count} از راهنمای اتاق ها حذف کنید؟",
 | 
					          "آیا مطمئنید که می خواهید این اتاق را از فهرست راهنمای حذف کنید؟ |||| آیا مطمئن هستید که می خواهید این موارد را %{smart_count} از راهنمای اتاق ها حذف کنید؟",
 | 
				
			||||||
        erase: "حذف از فهرست اتاق",
 | 
					        erase: "حذف از فهرست اتاق",
 | 
				
			||||||
| 
						 | 
					@ -1,21 +1,21 @@
 | 
				
			||||||
import frenchMessages from "ra-language-french";
 | 
					import frenchMessages from "ra-language-french";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const fr = {
 | 
					import { SynapseTranslationMessages } from ".";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const fr: SynapseTranslationMessages = {
 | 
				
			||||||
  ...frenchMessages,
 | 
					  ...frenchMessages,
 | 
				
			||||||
  synapseadmin: {
 | 
					  synapseadmin: {
 | 
				
			||||||
    auth: {
 | 
					    auth: {
 | 
				
			||||||
      base_url: "URL du serveur d’accueil",
 | 
					      base_url: "URL du serveur d’accueil",
 | 
				
			||||||
      welcome: "Bienvenue sur Synapse-admin",
 | 
					      welcome: "Bienvenue sur Synapse-admin",
 | 
				
			||||||
      server_version: "Version du serveur Synapse",
 | 
					      server_version: "Version du serveur Synapse",
 | 
				
			||||||
      username_error:
 | 
					      username_error: "Veuillez entrer un nom d'utilisateur complet : « @utilisateur:domaine »",
 | 
				
			||||||
        "Veuillez entrer un nom d'utilisateur complet : « @utilisateur:domaine »",
 | 
					 | 
				
			||||||
      protocol_error: "L'URL doit commencer par « http:// » ou « https:// »",
 | 
					      protocol_error: "L'URL doit commencer par « http:// » ou « https:// »",
 | 
				
			||||||
      url_error: "L'URL du serveur Matrix n'est pas valide",
 | 
					      url_error: "L'URL du serveur Matrix n'est pas valide",
 | 
				
			||||||
      sso_sign_in: "Se connecter avec l’authentification unique",
 | 
					      sso_sign_in: "Se connecter avec l’authentification unique",
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    users: {
 | 
					    users: {
 | 
				
			||||||
      invalid_user_id:
 | 
					      invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur d’accueil.",
 | 
				
			||||||
        "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur d’accueil.",
 | 
					 | 
				
			||||||
      tabs: { sso: "Authentification unique" },
 | 
					      tabs: { sso: "Authentification unique" },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    rooms: {
 | 
					    rooms: {
 | 
				
			||||||
| 
						 | 
					@ -35,8 +35,7 @@ const fr = {
 | 
				
			||||||
      required_field: "Le champ requis « %{field} » est manquant",
 | 
					      required_field: "Le champ requis « %{field} » est manquant",
 | 
				
			||||||
      invalid_value:
 | 
					      invalid_value:
 | 
				
			||||||
        "Valeur non valide à la ligne %{row}. Le champ « %{field} » ne peut être que « true » ou « false »",
 | 
					        "Valeur non valide à la ligne %{row}. Le champ « %{field} » ne peut être que « true » ou « false »",
 | 
				
			||||||
      unreasonably_big:
 | 
					      unreasonably_big: "Refus de charger un fichier trop volumineux de %{size} mégaoctets",
 | 
				
			||||||
        "Refus de charger un fichier trop volumineux de %{size} mégaoctets",
 | 
					 | 
				
			||||||
      already_in_progress: "Un import est déjà en cours",
 | 
					      already_in_progress: "Un import est déjà en cours",
 | 
				
			||||||
      id_exits: "L'identifiant %{id} déjà présent",
 | 
					      id_exits: "L'identifiant %{id} déjà présent",
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -48,8 +47,7 @@ const fr = {
 | 
				
			||||||
        users_total:
 | 
					        users_total:
 | 
				
			||||||
          "%{smart_count} utilisateur dans le fichier CSV |||| %{smart_count} utilisateurs dans le fichier CSV",
 | 
					          "%{smart_count} utilisateur dans le fichier CSV |||| %{smart_count} utilisateurs dans le fichier CSV",
 | 
				
			||||||
        guest_count: "%{smart_count} visiteur |||| %{smart_count} visiteurs",
 | 
					        guest_count: "%{smart_count} visiteur |||| %{smart_count} visiteurs",
 | 
				
			||||||
        admin_count:
 | 
					        admin_count: "%{smart_count} administrateur |||| %{smart_count} administrateurs",
 | 
				
			||||||
          "%{smart_count} administrateur |||| %{smart_count} administrateurs",
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      conflicts: {
 | 
					      conflicts: {
 | 
				
			||||||
        header: "Stratégie de résolution des conflits",
 | 
					        header: "Stratégie de résolution des conflits",
 | 
				
			||||||
| 
						 | 
					@ -61,11 +59,9 @@ const fr = {
 | 
				
			||||||
      ids: {
 | 
					      ids: {
 | 
				
			||||||
        header: "Identifiants",
 | 
					        header: "Identifiants",
 | 
				
			||||||
        all_ids_present: "Identifiants présents pour chaque entrée",
 | 
					        all_ids_present: "Identifiants présents pour chaque entrée",
 | 
				
			||||||
        count_ids_present:
 | 
					        count_ids_present: "%{smart_count} entrée avec identifiant |||| %{smart_count} entrées avec identifiant",
 | 
				
			||||||
          "%{smart_count} entrée avec identifiant |||| %{smart_count} entrées avec identifiant",
 | 
					 | 
				
			||||||
        mode: {
 | 
					        mode: {
 | 
				
			||||||
          ignore:
 | 
					          ignore: "Ignorer les identifiants dans le ficher CSV et en créer de nouveaux",
 | 
				
			||||||
            "Ignorer les identifiants dans le ficher CSV et en créer de nouveaux",
 | 
					 | 
				
			||||||
          update: "Mettre à jour les enregistrements existants",
 | 
					          update: "Mettre à jour les enregistrements existants",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
| 
						 | 
					@ -87,13 +83,11 @@ const fr = {
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      results: {
 | 
					      results: {
 | 
				
			||||||
        header: "Résultats de l'import",
 | 
					        header: "Résultats de l'import",
 | 
				
			||||||
        total:
 | 
					        total: "%{smart_count} entrée au total |||| %{smart_count} entrées au total",
 | 
				
			||||||
          "%{smart_count} entrée au total |||| %{smart_count} entrées au total",
 | 
					 | 
				
			||||||
        successful: "%{smart_count} entrées importées avec succès",
 | 
					        successful: "%{smart_count} entrées importées avec succès",
 | 
				
			||||||
        skipped: "%{smart_count} entrées ignorées",
 | 
					        skipped: "%{smart_count} entrées ignorées",
 | 
				
			||||||
        download_skipped: "Télécharger les entrées ignorées",
 | 
					        download_skipped: "Télécharger les entrées ignorées",
 | 
				
			||||||
        with_error:
 | 
					        with_error: "%{smart_count} entrée avec des erreurs ||| %{smart_count} entrées avec des erreurs",
 | 
				
			||||||
          "%{smart_count} entrée avec des erreurs ||| %{smart_count} entrées avec des erreurs",
 | 
					 | 
				
			||||||
        simulated_only: "L'import était simulé",
 | 
					        simulated_only: "L'import était simulé",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -110,6 +104,7 @@ const fr = {
 | 
				
			||||||
        name: "Nom",
 | 
					        name: "Nom",
 | 
				
			||||||
        is_guest: "Visiteur",
 | 
					        is_guest: "Visiteur",
 | 
				
			||||||
        admin: "Administrateur du serveur",
 | 
					        admin: "Administrateur du serveur",
 | 
				
			||||||
 | 
					        locked: "Verrouillé",
 | 
				
			||||||
        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",
 | 
				
			||||||
| 
						 | 
					@ -126,8 +121,7 @@ const fr = {
 | 
				
			||||||
        auth_provider: "Fournisseur d'identité",
 | 
					        auth_provider: "Fournisseur d'identité",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      helper: {
 | 
					      helper: {
 | 
				
			||||||
        deactivate:
 | 
					        deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
 | 
				
			||||||
          "Vous devrez fournir un mot de passe pour réactiver le compte.",
 | 
					 | 
				
			||||||
        erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
 | 
					        erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      action: {
 | 
					      action: {
 | 
				
			||||||
| 
						 | 
					@ -341,13 +335,11 @@ const fr = {
 | 
				
			||||||
    room_directory: {
 | 
					    room_directory: {
 | 
				
			||||||
      name: "Répertoire des salons",
 | 
					      name: "Répertoire des salons",
 | 
				
			||||||
      fields: {
 | 
					      fields: {
 | 
				
			||||||
        world_readable:
 | 
					        world_readable: "Tout utilisateur peut avoir un aperçu du salon, sans en devenir membre",
 | 
				
			||||||
          "Tout utilisateur peut avoir un aperçu du salon, sans en devenir membre",
 | 
					 | 
				
			||||||
        guest_can_join: "Les visiteurs peuvent rejoindre le salon",
 | 
					        guest_can_join: "Les visiteurs peuvent rejoindre le salon",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      action: {
 | 
					      action: {
 | 
				
			||||||
        title:
 | 
					        title: "Supprimer un salon du répertoire |||| Supprimer %{smart_count} salons du répertoire",
 | 
				
			||||||
          "Supprimer un salon du répertoire |||| Supprimer %{smart_count} salons du répertoire",
 | 
					 | 
				
			||||||
        content:
 | 
					        content:
 | 
				
			||||||
          "Voulez-vous vraiment supprimer ce salon du répertoire ? |||| Voulez-vous vraiment supprimer ces %{smart_count} salons du répertoire ?",
 | 
					          "Voulez-vous vraiment supprimer ce salon du répertoire ? |||| Voulez-vous vraiment supprimer ces %{smart_count} salons du répertoire ?",
 | 
				
			||||||
        erase: "Supprimer du répertoire des salons",
 | 
					        erase: "Supprimer du répertoire des salons",
 | 
				
			||||||
| 
						 | 
					@ -368,8 +360,7 @@ const fr = {
 | 
				
			||||||
        length: "Longueur",
 | 
					        length: "Longueur",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      helper: {
 | 
					      helper: {
 | 
				
			||||||
        length:
 | 
					        length: "Longueur du jeton généré aléatoirement si aucun jeton n'est pas spécifié",
 | 
				
			||||||
          "Longueur du jeton généré aléatoirement si aucun jeton n'est pas spécifié",
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
							
								
								
									
										391
									
								
								src/i18n/index.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										391
									
								
								src/i18n/index.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,391 @@
 | 
				
			||||||
 | 
					import { TranslationMessages } from "ra-core";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SynapseTranslationMessages extends TranslationMessages {
 | 
				
			||||||
 | 
					  synapseadmin: {
 | 
				
			||||||
 | 
					    auth: {
 | 
				
			||||||
 | 
					      base_url: string;
 | 
				
			||||||
 | 
					      welcome: string;
 | 
				
			||||||
 | 
					      server_version: string;
 | 
				
			||||||
 | 
					      supports_specs?: string; // TODO: fa, fr, it, zh
 | 
				
			||||||
 | 
					      username_error: string;
 | 
				
			||||||
 | 
					      protocol_error: string;
 | 
				
			||||||
 | 
					      url_error: string;
 | 
				
			||||||
 | 
					      sso_sign_in: string;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    users: {
 | 
				
			||||||
 | 
					      invalid_user_id: string;
 | 
				
			||||||
 | 
					      tabs: { sso: string };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    rooms: {
 | 
				
			||||||
 | 
					      details?: string; // TODO: fa, fr, it, zh
 | 
				
			||||||
 | 
					      tabs: {
 | 
				
			||||||
 | 
					        basic: string;
 | 
				
			||||||
 | 
					        members: string;
 | 
				
			||||||
 | 
					        detail: string;
 | 
				
			||||||
 | 
					        permission: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    reports: { tabs: { basic: string; detail: string } };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  import_users: {
 | 
				
			||||||
 | 
					    error: {
 | 
				
			||||||
 | 
					      at_entry: string;
 | 
				
			||||||
 | 
					      error: string;
 | 
				
			||||||
 | 
					      required_field: string;
 | 
				
			||||||
 | 
					      invalid_value: string;
 | 
				
			||||||
 | 
					      unreasonably_big: string;
 | 
				
			||||||
 | 
					      already_in_progress: string;
 | 
				
			||||||
 | 
					      id_exits: string;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    goToPdf: string;
 | 
				
			||||||
 | 
					    cards: {
 | 
				
			||||||
 | 
					      importstats: {
 | 
				
			||||||
 | 
					        header: string;
 | 
				
			||||||
 | 
					        users_total: string;
 | 
				
			||||||
 | 
					        guest_count: string;
 | 
				
			||||||
 | 
					        admin_count: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      conflicts: {
 | 
				
			||||||
 | 
					        header: string;
 | 
				
			||||||
 | 
					        mode: {
 | 
				
			||||||
 | 
					          stop: string;
 | 
				
			||||||
 | 
					          skip: string;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      ids: {
 | 
				
			||||||
 | 
					        header: string;
 | 
				
			||||||
 | 
					        all_ids_present: string;
 | 
				
			||||||
 | 
					        count_ids_present: string;
 | 
				
			||||||
 | 
					        mode: {
 | 
				
			||||||
 | 
					          ignore: string;
 | 
				
			||||||
 | 
					          update: string;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      passwords: {
 | 
				
			||||||
 | 
					        header: string;
 | 
				
			||||||
 | 
					        all_passwords_present: string;
 | 
				
			||||||
 | 
					        count_passwords_present: string;
 | 
				
			||||||
 | 
					        use_passwords: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      upload: {
 | 
				
			||||||
 | 
					        header: string;
 | 
				
			||||||
 | 
					        explanation: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      startImport: {
 | 
				
			||||||
 | 
					        simulate_only: string;
 | 
				
			||||||
 | 
					        run_import: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      results: {
 | 
				
			||||||
 | 
					        header: string;
 | 
				
			||||||
 | 
					        total: string;
 | 
				
			||||||
 | 
					        successful: string;
 | 
				
			||||||
 | 
					        skipped: string;
 | 
				
			||||||
 | 
					        download_skipped: string;
 | 
				
			||||||
 | 
					        with_error: string;
 | 
				
			||||||
 | 
					        simulated_only: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  resources: {
 | 
				
			||||||
 | 
					    users: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      email: string;
 | 
				
			||||||
 | 
					      msisdn: string;
 | 
				
			||||||
 | 
					      threepid: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        avatar: string;
 | 
				
			||||||
 | 
					        id: string;
 | 
				
			||||||
 | 
					        name: string;
 | 
				
			||||||
 | 
					        is_guest: string;
 | 
				
			||||||
 | 
					        admin: string;
 | 
				
			||||||
 | 
					        locked?: string; // TODO: fa, zh
 | 
				
			||||||
 | 
					        deactivated: string;
 | 
				
			||||||
 | 
					        erased?: string; // TODO: fa, fr, it, zh
 | 
				
			||||||
 | 
					        guests: string;
 | 
				
			||||||
 | 
					        show_deactivated: string;
 | 
				
			||||||
 | 
					        user_id: string;
 | 
				
			||||||
 | 
					        displayname: string;
 | 
				
			||||||
 | 
					        password: string;
 | 
				
			||||||
 | 
					        avatar_url: string;
 | 
				
			||||||
 | 
					        avatar_src: string;
 | 
				
			||||||
 | 
					        medium: string;
 | 
				
			||||||
 | 
					        threepids: string;
 | 
				
			||||||
 | 
					        address: string;
 | 
				
			||||||
 | 
					        creation_ts_ms: string;
 | 
				
			||||||
 | 
					        consent_version: string;
 | 
				
			||||||
 | 
					        auth_provider?: string;
 | 
				
			||||||
 | 
					        user_type?: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      helper: {
 | 
				
			||||||
 | 
					        password?: string;
 | 
				
			||||||
 | 
					        deactivate: string;
 | 
				
			||||||
 | 
					        erase: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      action: {
 | 
				
			||||||
 | 
					        erase: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    rooms: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        room_id: string;
 | 
				
			||||||
 | 
					        name: string;
 | 
				
			||||||
 | 
					        canonical_alias: string;
 | 
				
			||||||
 | 
					        joined_members: string;
 | 
				
			||||||
 | 
					        joined_local_members: string;
 | 
				
			||||||
 | 
					        joined_local_devices?: string;
 | 
				
			||||||
 | 
					        state_events: string;
 | 
				
			||||||
 | 
					        version: string;
 | 
				
			||||||
 | 
					        is_encrypted: string;
 | 
				
			||||||
 | 
					        encryption: string;
 | 
				
			||||||
 | 
					        federatable: string;
 | 
				
			||||||
 | 
					        public: string;
 | 
				
			||||||
 | 
					        creator: string;
 | 
				
			||||||
 | 
					        join_rules: string;
 | 
				
			||||||
 | 
					        guest_access: string;
 | 
				
			||||||
 | 
					        history_visibility: string;
 | 
				
			||||||
 | 
					        topic?: string;
 | 
				
			||||||
 | 
					        avatar?: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      helper?: {
 | 
				
			||||||
 | 
					        forward_extremities: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      enums: {
 | 
				
			||||||
 | 
					        join_rules: {
 | 
				
			||||||
 | 
					          public: string;
 | 
				
			||||||
 | 
					          knock: string;
 | 
				
			||||||
 | 
					          invite: string;
 | 
				
			||||||
 | 
					          private: string;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        guest_access: {
 | 
				
			||||||
 | 
					          can_join: string;
 | 
				
			||||||
 | 
					          forbidden: string;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        history_visibility: {
 | 
				
			||||||
 | 
					          invited: string;
 | 
				
			||||||
 | 
					          joined: string;
 | 
				
			||||||
 | 
					          shared: string;
 | 
				
			||||||
 | 
					          world_readable: string;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        unencrypted: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      action?: {
 | 
				
			||||||
 | 
					        erase: {
 | 
				
			||||||
 | 
					          title: string;
 | 
				
			||||||
 | 
					          content: string;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    reports: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        id: string;
 | 
				
			||||||
 | 
					        received_ts: string;
 | 
				
			||||||
 | 
					        user_id: string;
 | 
				
			||||||
 | 
					        name: string;
 | 
				
			||||||
 | 
					        score: string;
 | 
				
			||||||
 | 
					        reason: string;
 | 
				
			||||||
 | 
					        event_id: string;
 | 
				
			||||||
 | 
					        event_json: {
 | 
				
			||||||
 | 
					          origin: string;
 | 
				
			||||||
 | 
					          origin_server_ts: string;
 | 
				
			||||||
 | 
					          type: string;
 | 
				
			||||||
 | 
					          content: {
 | 
				
			||||||
 | 
					            msgtype: string;
 | 
				
			||||||
 | 
					            body: string;
 | 
				
			||||||
 | 
					            format: string;
 | 
				
			||||||
 | 
					            formatted_body: string;
 | 
				
			||||||
 | 
					            algorithm: string;
 | 
				
			||||||
 | 
					            url?: string;
 | 
				
			||||||
 | 
					            info?: {
 | 
				
			||||||
 | 
					              mimetype: string;
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      action?: {
 | 
				
			||||||
 | 
					        erase: {
 | 
				
			||||||
 | 
					          title: string;
 | 
				
			||||||
 | 
					          content: string;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    connections: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        last_seen: string;
 | 
				
			||||||
 | 
					        ip: string;
 | 
				
			||||||
 | 
					        user_agent: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    devices: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        device_id: string;
 | 
				
			||||||
 | 
					        display_name: string;
 | 
				
			||||||
 | 
					        last_seen_ts: string;
 | 
				
			||||||
 | 
					        last_seen_ip: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      action: {
 | 
				
			||||||
 | 
					        erase: {
 | 
				
			||||||
 | 
					          title: string;
 | 
				
			||||||
 | 
					          content: string;
 | 
				
			||||||
 | 
					          success: string;
 | 
				
			||||||
 | 
					          failure: string;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    users_media: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        media_id: string;
 | 
				
			||||||
 | 
					        media_length: string;
 | 
				
			||||||
 | 
					        media_type: string;
 | 
				
			||||||
 | 
					        upload_name: string;
 | 
				
			||||||
 | 
					        quarantined_by: string;
 | 
				
			||||||
 | 
					        safe_from_quarantine: string;
 | 
				
			||||||
 | 
					        created_ts: string;
 | 
				
			||||||
 | 
					        last_access_ts: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      action?: {
 | 
				
			||||||
 | 
					        open: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    delete_media: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        before_ts: string;
 | 
				
			||||||
 | 
					        size_gt: string;
 | 
				
			||||||
 | 
					        keep_profiles: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      action: {
 | 
				
			||||||
 | 
					        send: string;
 | 
				
			||||||
 | 
					        send_success: string;
 | 
				
			||||||
 | 
					        send_failure: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      helper: {
 | 
				
			||||||
 | 
					        send: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    protect_media?: {
 | 
				
			||||||
 | 
					      action: {
 | 
				
			||||||
 | 
					        create: string;
 | 
				
			||||||
 | 
					        delete: string;
 | 
				
			||||||
 | 
					        none: string;
 | 
				
			||||||
 | 
					        send_success: string;
 | 
				
			||||||
 | 
					        send_failure: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    quarantine_media?: {
 | 
				
			||||||
 | 
					      action: {
 | 
				
			||||||
 | 
					        name: string;
 | 
				
			||||||
 | 
					        create: string;
 | 
				
			||||||
 | 
					        delete: string;
 | 
				
			||||||
 | 
					        none: string;
 | 
				
			||||||
 | 
					        send_success: string;
 | 
				
			||||||
 | 
					        send_failure: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    pushers: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        app: string;
 | 
				
			||||||
 | 
					        app_display_name: string;
 | 
				
			||||||
 | 
					        app_id: string;
 | 
				
			||||||
 | 
					        device_display_name: string;
 | 
				
			||||||
 | 
					        kind: string;
 | 
				
			||||||
 | 
					        lang: string;
 | 
				
			||||||
 | 
					        profile_tag: string;
 | 
				
			||||||
 | 
					        pushkey: string;
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          url: string;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    servernotices: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      send: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        body: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      action: {
 | 
				
			||||||
 | 
					        send: string;
 | 
				
			||||||
 | 
					        send_success: string;
 | 
				
			||||||
 | 
					        send_failure: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      helper: {
 | 
				
			||||||
 | 
					        send: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    user_media_statistics: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        media_count: string;
 | 
				
			||||||
 | 
					        media_length: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    forward_extremities?: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        id: string;
 | 
				
			||||||
 | 
					        received_ts: string;
 | 
				
			||||||
 | 
					        depth: string;
 | 
				
			||||||
 | 
					        state_group: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    room_state?: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        type: string;
 | 
				
			||||||
 | 
					        content: string;
 | 
				
			||||||
 | 
					        origin_server_ts: string;
 | 
				
			||||||
 | 
					        sender: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    room_directory?: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        world_readable: string;
 | 
				
			||||||
 | 
					        guest_can_join: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      action: {
 | 
				
			||||||
 | 
					        title: string;
 | 
				
			||||||
 | 
					        content: string;
 | 
				
			||||||
 | 
					        erase: string;
 | 
				
			||||||
 | 
					        create: string;
 | 
				
			||||||
 | 
					        send_success: string;
 | 
				
			||||||
 | 
					        send_failure: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    destinations?: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        destination: string;
 | 
				
			||||||
 | 
					        failure_ts: string;
 | 
				
			||||||
 | 
					        retry_last_ts: string;
 | 
				
			||||||
 | 
					        retry_interval: string;
 | 
				
			||||||
 | 
					        last_successful_stream_ordering: string;
 | 
				
			||||||
 | 
					        stream_ordering: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      action: {
 | 
				
			||||||
 | 
					        reconnect: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    registration_tokens?: {
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      fields: {
 | 
				
			||||||
 | 
					        token: string;
 | 
				
			||||||
 | 
					        valid: string;
 | 
				
			||||||
 | 
					        uses_allowed: string;
 | 
				
			||||||
 | 
					        pending: string;
 | 
				
			||||||
 | 
					        completed: string;
 | 
				
			||||||
 | 
					        expiry_time: string;
 | 
				
			||||||
 | 
					        length: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      helper: {
 | 
				
			||||||
 | 
					        length: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,15 @@
 | 
				
			||||||
import italianMessages from "ra-language-italian";
 | 
					import italianMessages from "ra-language-italian";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const it = {
 | 
					import { SynapseTranslationMessages } from ".";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const it: SynapseTranslationMessages = {
 | 
				
			||||||
  ...italianMessages,
 | 
					  ...italianMessages,
 | 
				
			||||||
  synapseadmin: {
 | 
					  synapseadmin: {
 | 
				
			||||||
    auth: {
 | 
					    auth: {
 | 
				
			||||||
      base_url: "URL dell'homeserver",
 | 
					      base_url: "URL dell'homeserver",
 | 
				
			||||||
      welcome: "Benvenuto in Synapse-admin",
 | 
					      welcome: "Benvenuto in Synapse-admin",
 | 
				
			||||||
      server_version: "Versione di Synapse",
 | 
					      server_version: "Versione di Synapse",
 | 
				
			||||||
      username_error:
 | 
					      username_error: "Per favore inserisci un ID utente completo: '@utente:dominio'",
 | 
				
			||||||
        "Per favore inserisci un ID utente completo: '@utente:dominio'",
 | 
					 | 
				
			||||||
      protocol_error: "L'URL deve iniziare per 'http://' o 'https://'",
 | 
					      protocol_error: "L'URL deve iniziare per 'http://' o 'https://'",
 | 
				
			||||||
      url_error: "URL del server Matrix non valido",
 | 
					      url_error: "URL del server Matrix non valido",
 | 
				
			||||||
      sso_sign_in: "Accedi con SSO",
 | 
					      sso_sign_in: "Accedi con SSO",
 | 
				
			||||||
| 
						 | 
					@ -32,10 +33,8 @@ const it = {
 | 
				
			||||||
      at_entry: "Alla voce %{entry}: %{message}",
 | 
					      at_entry: "Alla voce %{entry}: %{message}",
 | 
				
			||||||
      error: "Errore",
 | 
					      error: "Errore",
 | 
				
			||||||
      required_field: "Il campo '%{field}' non è presente",
 | 
					      required_field: "Il campo '%{field}' non è presente",
 | 
				
			||||||
      invalid_value:
 | 
					      invalid_value: "Valore non valido alla riga %{row}. '%{field}' Il campo può essere solo 'true' o 'false'",
 | 
				
			||||||
        "Valore non valido alla riga %{row}. '%{field}' Il campo può essere solo 'true' o 'false'",
 | 
					      unreasonably_big: "Impossibile caricare un file così grosso (%{size} megabyte)",
 | 
				
			||||||
      unreasonably_big:
 | 
					 | 
				
			||||||
        "Impossibile caricare un file così grosso (%{size} megabyte)",
 | 
					 | 
				
			||||||
      already_in_progress: "Un import è attualmente già in caricamento",
 | 
					      already_in_progress: "Un import è attualmente già in caricamento",
 | 
				
			||||||
      id_exits: "L'ID %{id} è già presente",
 | 
					      id_exits: "L'ID %{id} è già presente",
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -44,11 +43,9 @@ const it = {
 | 
				
			||||||
    cards: {
 | 
					    cards: {
 | 
				
			||||||
      importstats: {
 | 
					      importstats: {
 | 
				
			||||||
        header: "Importa utenti",
 | 
					        header: "Importa utenti",
 | 
				
			||||||
        users_total:
 | 
					        users_total: "%{smart_count} utente nel file CSV |||| %{smart_count} utenti nel file CSV",
 | 
				
			||||||
          "%{smart_count} utente nel file CSV |||| %{smart_count} utenti nel file CSV",
 | 
					 | 
				
			||||||
        guest_count: "%{smart_count} ospite |||| %{smart_count} ospiti",
 | 
					        guest_count: "%{smart_count} ospite |||| %{smart_count} ospiti",
 | 
				
			||||||
        admin_count:
 | 
					        admin_count: "%{smart_count} amministratore |||| %{smart_count} amministratori",
 | 
				
			||||||
          "%{smart_count} amministratore |||| %{smart_count} amministratori",
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      conflicts: {
 | 
					      conflicts: {
 | 
				
			||||||
        header: "Strategia di conflitto",
 | 
					        header: "Strategia di conflitto",
 | 
				
			||||||
| 
						 | 
					@ -60,8 +57,7 @@ const it = {
 | 
				
			||||||
      ids: {
 | 
					      ids: {
 | 
				
			||||||
        header: "ID",
 | 
					        header: "ID",
 | 
				
			||||||
        all_ids_present: "ID presenti in ogni voce",
 | 
					        all_ids_present: "ID presenti in ogni voce",
 | 
				
			||||||
        count_ids_present:
 | 
					        count_ids_present: "%{smart_count} voce con ID |||| %{smart_count} voci con ID",
 | 
				
			||||||
          "%{smart_count} voce con ID |||| %{smart_count} voci con ID",
 | 
					 | 
				
			||||||
        mode: {
 | 
					        mode: {
 | 
				
			||||||
          ignore: "Ignora gli ID nel file CSV e creane di nuovi",
 | 
					          ignore: "Ignora gli ID nel file CSV e creane di nuovi",
 | 
				
			||||||
          update: "Aggiorna le voci esistenti",
 | 
					          update: "Aggiorna le voci esistenti",
 | 
				
			||||||
| 
						 | 
					@ -70,8 +66,7 @@ const it = {
 | 
				
			||||||
      passwords: {
 | 
					      passwords: {
 | 
				
			||||||
        header: "Passwords",
 | 
					        header: "Passwords",
 | 
				
			||||||
        all_passwords_present: "Password presenti in ogni voce",
 | 
					        all_passwords_present: "Password presenti in ogni voce",
 | 
				
			||||||
        count_passwords_present:
 | 
					        count_passwords_present: "%{smart_count} voce con password |||| %{smart_count} voci con password",
 | 
				
			||||||
          "%{smart_count} voce con password |||| %{smart_count} voci con password",
 | 
					 | 
				
			||||||
        use_passwords: "Usa le password dal file CSV",
 | 
					        use_passwords: "Usa le password dal file CSV",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      upload: {
 | 
					      upload: {
 | 
				
			||||||
| 
						 | 
					@ -85,13 +80,11 @@ const it = {
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      results: {
 | 
					      results: {
 | 
				
			||||||
        header: "Importa i risultati",
 | 
					        header: "Importa i risultati",
 | 
				
			||||||
        total:
 | 
					        total: "%{smart_count} voce in totale |||| %{smart_count} voci in totale",
 | 
				
			||||||
          "%{smart_count} voce in totale |||| %{smart_count} voci in totale",
 | 
					 | 
				
			||||||
        successful: "%{smart_count} voci importate con successo",
 | 
					        successful: "%{smart_count} voci importate con successo",
 | 
				
			||||||
        skipped: "%{smart_count} voci ignorate",
 | 
					        skipped: "%{smart_count} voci ignorate",
 | 
				
			||||||
        download_skipped: "Scarica le voci ignorate",
 | 
					        download_skipped: "Scarica le voci ignorate",
 | 
				
			||||||
        with_error:
 | 
					        with_error: "%{smart_count} voce con errori ||| %{smart_count} voci con errori",
 | 
				
			||||||
          "%{smart_count} voce con errori ||| %{smart_count} voci con errori",
 | 
					 | 
				
			||||||
        simulated_only: "Il processo era stato solamente simulato",
 | 
					        simulated_only: "Il processo era stato solamente simulato",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -108,6 +101,7 @@ const it = {
 | 
				
			||||||
        name: "Nome",
 | 
					        name: "Nome",
 | 
				
			||||||
        is_guest: "Ospite",
 | 
					        is_guest: "Ospite",
 | 
				
			||||||
        admin: "Amministratore",
 | 
					        admin: "Amministratore",
 | 
				
			||||||
 | 
					        locked: "Bloccato",
 | 
				
			||||||
        deactivated: "Disattivato",
 | 
					        deactivated: "Disattivato",
 | 
				
			||||||
        guests: "Mostra gli ospiti",
 | 
					        guests: "Mostra gli ospiti",
 | 
				
			||||||
        show_deactivated: "Mostra gli utenti disattivati",
 | 
					        show_deactivated: "Mostra gli utenti disattivati",
 | 
				
			||||||
| 
						 | 
					@ -125,8 +119,7 @@ const it = {
 | 
				
			||||||
        user_type: "Tipo d'utente",
 | 
					        user_type: "Tipo d'utente",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      helper: {
 | 
					      helper: {
 | 
				
			||||||
        password:
 | 
					        password: "Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
 | 
				
			||||||
          "Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
 | 
					 | 
				
			||||||
        deactivate: "Devi fornire una password per riattivare l'account.",
 | 
					        deactivate: "Devi fornire una password per riattivare l'account.",
 | 
				
			||||||
        erase: "Constrassegna l'utente come cancellato dal GDPR",
 | 
					        erase: "Constrassegna l'utente come cancellato dal GDPR",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
| 
						 | 
					@ -345,8 +338,7 @@ const it = {
 | 
				
			||||||
        guest_can_join: "gli utenti ospite possono entrare",
 | 
					        guest_can_join: "gli utenti ospite possono entrare",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      action: {
 | 
					      action: {
 | 
				
			||||||
        title:
 | 
					        title: "Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
 | 
				
			||||||
          "Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
 | 
					 | 
				
			||||||
        content:
 | 
					        content:
 | 
				
			||||||
          "Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
 | 
					          "Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
 | 
				
			||||||
        erase: "Rimuovi dall'elenco",
 | 
					        erase: "Rimuovi dall'elenco",
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
import chineseMessages from "@haxqer/ra-language-chinese";
 | 
					import chineseMessages from "@haxqer/ra-language-chinese";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const zh = {
 | 
					import { SynapseTranslationMessages } from ".";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const zh: SynapseTranslationMessages = {
 | 
				
			||||||
  ...chineseMessages,
 | 
					  ...chineseMessages,
 | 
				
			||||||
  synapseadmin: {
 | 
					  synapseadmin: {
 | 
				
			||||||
    auth: {
 | 
					    auth: {
 | 
				
			||||||
| 
						 | 
					@ -13,8 +15,7 @@ const zh = {
 | 
				
			||||||
      sso_sign_in: "使用 SSO 登录",
 | 
					      sso_sign_in: "使用 SSO 登录",
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    users: {
 | 
					    users: {
 | 
				
			||||||
      invalid_user_id:
 | 
					      invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
 | 
				
			||||||
        "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
 | 
					 | 
				
			||||||
      tabs: { sso: "SSO" },
 | 
					      tabs: { sso: "SSO" },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    rooms: {
 | 
					    rooms: {
 | 
				
			||||||
| 
						 | 
					@ -24,11 +25,6 @@ const zh = {
 | 
				
			||||||
        detail: "细节",
 | 
					        detail: "细节",
 | 
				
			||||||
        permission: "权限",
 | 
					        permission: "权限",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      delete: {
 | 
					 | 
				
			||||||
        title: "删除房间",
 | 
					 | 
				
			||||||
        message:
 | 
					 | 
				
			||||||
          "您确定要删除这个房间吗?该操作无法被撤销。这个房间里所有的消息和分享的媒体都将被从服务器上删除!",
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    reports: { tabs: { basic: "基本", detail: "细节" } },
 | 
					    reports: { tabs: { basic: "基本", detail: "细节" } },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -37,8 +33,7 @@ const zh = {
 | 
				
			||||||
      at_entry: "在条目 %{entry}: %{message}",
 | 
					      at_entry: "在条目 %{entry}: %{message}",
 | 
				
			||||||
      error: "错误",
 | 
					      error: "错误",
 | 
				
			||||||
      required_field: "需要的值 '%{field}' 未被设置。",
 | 
					      required_field: "需要的值 '%{field}' 未被设置。",
 | 
				
			||||||
      invalid_value:
 | 
					      invalid_value: "第 %{row} 行出现无效值。 '%{field}' 只可以是 'true' 或 'false'。",
 | 
				
			||||||
        "第 %{row} 行出现无效值。 '%{field}' 只可以是 'true' 或 'false'。",
 | 
					 | 
				
			||||||
      unreasonably_big: "拒绝加载过大的文件: %{size} MB",
 | 
					      unreasonably_big: "拒绝加载过大的文件: %{size} MB",
 | 
				
			||||||
      already_in_progress: "一个导入进程已经在运行中",
 | 
					      already_in_progress: "一个导入进程已经在运行中",
 | 
				
			||||||
      id_exits: "ID %{id} 已经存在",
 | 
					      id_exits: "ID %{id} 已经存在",
 | 
				
			||||||
| 
						 | 
					@ -48,8 +43,7 @@ const zh = {
 | 
				
			||||||
    cards: {
 | 
					    cards: {
 | 
				
			||||||
      importstats: {
 | 
					      importstats: {
 | 
				
			||||||
        header: "导入用户",
 | 
					        header: "导入用户",
 | 
				
			||||||
        users_total:
 | 
					        users_total: "%{smart_count} 用户在 CSV 文件中 |||| %{smart_count} 用户在 CSV 文件中",
 | 
				
			||||||
          "%{smart_count} 用户在 CSV 文件中 |||| %{smart_count} 用户在 CSV 文件中",
 | 
					 | 
				
			||||||
        guest_count: "%{smart_count} 访客 |||| %{smart_count} 访客",
 | 
					        guest_count: "%{smart_count} 访客 |||| %{smart_count} 访客",
 | 
				
			||||||
        admin_count: "%{smart_count} 管理员 |||| %{smart_count} 管理员",
 | 
					        admin_count: "%{smart_count} 管理员 |||| %{smart_count} 管理员",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
| 
						 | 
					@ -63,8 +57,7 @@ const zh = {
 | 
				
			||||||
      ids: {
 | 
					      ids: {
 | 
				
			||||||
        header: "IDs",
 | 
					        header: "IDs",
 | 
				
			||||||
        all_ids_present: "每条记录的 ID",
 | 
					        all_ids_present: "每条记录的 ID",
 | 
				
			||||||
        count_ids_present:
 | 
					        count_ids_present: "%{smart_count} 个含 ID 的记录 |||| %{smart_count} 个含 ID 的记录",
 | 
				
			||||||
          "%{smart_count} 个含 ID 的记录 |||| %{smart_count} 个含 ID 的记录",
 | 
					 | 
				
			||||||
        mode: {
 | 
					        mode: {
 | 
				
			||||||
          ignore: "忽略 CSV 中的 ID 并创建新的",
 | 
					          ignore: "忽略 CSV 中的 ID 并创建新的",
 | 
				
			||||||
          update: "更新已经存在的记录",
 | 
					          update: "更新已经存在的记录",
 | 
				
			||||||
| 
						 | 
					@ -73,8 +66,7 @@ const zh = {
 | 
				
			||||||
      passwords: {
 | 
					      passwords: {
 | 
				
			||||||
        header: "密码",
 | 
					        header: "密码",
 | 
				
			||||||
        all_passwords_present: "每条记录的密码",
 | 
					        all_passwords_present: "每条记录的密码",
 | 
				
			||||||
        count_passwords_present:
 | 
					        count_passwords_present: "%{smart_count} 个含密码的记录 |||| %{smart_count} 个含密码的记录",
 | 
				
			||||||
          "%{smart_count} 个含密码的记录 |||| %{smart_count} 个含密码的记录",
 | 
					 | 
				
			||||||
        use_passwords: "使用 CSV 中标记的密码",
 | 
					        use_passwords: "使用 CSV 中标记的密码",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      upload: {
 | 
					      upload: {
 | 
				
			||||||
| 
						 | 
					@ -92,8 +84,7 @@ const zh = {
 | 
				
			||||||
        successful: "%{smart_count} 条记录导入成功",
 | 
					        successful: "%{smart_count} 条记录导入成功",
 | 
				
			||||||
        skipped: "跳过 %{smart_count} 条记录",
 | 
					        skipped: "跳过 %{smart_count} 条记录",
 | 
				
			||||||
        download_skipped: "下载跳过的记录",
 | 
					        download_skipped: "下载跳过的记录",
 | 
				
			||||||
        with_error:
 | 
					        with_error: "%{smart_count} 条记录出现错误 ||| %{smart_count} 条记录出现错误",
 | 
				
			||||||
          "%{smart_count} 条记录出现错误 ||| %{smart_count} 条记录出现错误",
 | 
					 | 
				
			||||||
        simulated_only: "只是一次模拟运行",
 | 
					        simulated_only: "只是一次模拟运行",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { createRoot } from "react-dom/client";
 | 
					import { createRoot } from "react-dom/client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import App from "./App";
 | 
					import App from "./App";
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/jest.setup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/jest.setup.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					import "@testing-library/jest-dom";
 | 
				
			||||||
| 
						 | 
					@ -1,3 +0,0 @@
 | 
				
			||||||
import fetchMock from "jest-fetch-mock";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fetchMock.enableMocks();
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,18 @@
 | 
				
			||||||
 | 
					import fetchMock from "jest-fetch-mock";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import authProvider from "./authProvider";
 | 
					import authProvider from "./authProvider";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fetchMock.enableMocks();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe("authProvider", () => {
 | 
					describe("authProvider", () => {
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    fetch.resetMocks();
 | 
					    fetchMock.resetMocks();
 | 
				
			||||||
    localStorage.clear();
 | 
					    localStorage.clear();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe("login", () => {
 | 
					  describe("login", () => {
 | 
				
			||||||
    it("should successfully login with username and password", async () => {
 | 
					    it("should successfully login with username and password", async () => {
 | 
				
			||||||
      fetch.once(
 | 
					      fetchMock.once(
 | 
				
			||||||
        JSON.stringify({
 | 
					        JSON.stringify({
 | 
				
			||||||
          home_server: "example.com",
 | 
					          home_server: "example.com",
 | 
				
			||||||
          user_id: "@user:example.com",
 | 
					          user_id: "@user:example.com",
 | 
				
			||||||
| 
						 | 
					@ -17,24 +21,21 @@ describe("authProvider", () => {
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const ret = await authProvider.login({
 | 
					      const ret: undefined = await authProvider.login({
 | 
				
			||||||
        base_url: "http://example.com",
 | 
					        base_url: "http://example.com",
 | 
				
			||||||
        username: "@user:example.com",
 | 
					        username: "@user:example.com",
 | 
				
			||||||
        password: "secret",
 | 
					        password: "secret",
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(ret).toBe(undefined);
 | 
					      expect(ret).toBe(undefined);
 | 
				
			||||||
      expect(fetch).toBeCalledWith(
 | 
					      expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
 | 
				
			||||||
        "http://example.com/_matrix/client/r0/login",
 | 
					        body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","user":"@user:example.com","password":"secret"}',
 | 
				
			||||||
        {
 | 
					        headers: new Headers({
 | 
				
			||||||
          body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","user":"@user:example.com","password":"secret"}',
 | 
					          Accept: "application/json",
 | 
				
			||||||
          headers: new Headers({
 | 
					          "Content-Type": "application/json",
 | 
				
			||||||
            Accept: ["application/json"],
 | 
					        }),
 | 
				
			||||||
            "Content-Type": ["application/json"],
 | 
					        method: "POST",
 | 
				
			||||||
          }),
 | 
					      });
 | 
				
			||||||
          method: "POST",
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      expect(localStorage.getItem("base_url")).toEqual("http://example.com");
 | 
					      expect(localStorage.getItem("base_url")).toEqual("http://example.com");
 | 
				
			||||||
      expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
 | 
					      expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
 | 
				
			||||||
      expect(localStorage.getItem("access_token")).toEqual("foobar");
 | 
					      expect(localStorage.getItem("access_token")).toEqual("foobar");
 | 
				
			||||||
| 
						 | 
					@ -43,7 +44,7 @@ describe("authProvider", () => {
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it("should successfully login with token", async () => {
 | 
					  it("should successfully login with token", async () => {
 | 
				
			||||||
    fetch.once(
 | 
					    fetchMock.once(
 | 
				
			||||||
      JSON.stringify({
 | 
					      JSON.stringify({
 | 
				
			||||||
        home_server: "example.com",
 | 
					        home_server: "example.com",
 | 
				
			||||||
        user_id: "@user:example.com",
 | 
					        user_id: "@user:example.com",
 | 
				
			||||||
| 
						 | 
					@ -52,23 +53,20 @@ describe("authProvider", () => {
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ret = await authProvider.login({
 | 
					    const ret: undefined = await authProvider.login({
 | 
				
			||||||
      base_url: "https://example.com/",
 | 
					      base_url: "https://example.com/",
 | 
				
			||||||
      loginToken: "login_token",
 | 
					      loginToken: "login_token",
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(ret).toBe(undefined);
 | 
					    expect(ret).toBe(undefined);
 | 
				
			||||||
    expect(fetch).toBeCalledWith(
 | 
					    expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/r0/login", {
 | 
				
			||||||
      "https://example.com/_matrix/client/r0/login",
 | 
					      body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
 | 
				
			||||||
      {
 | 
					      headers: new Headers({
 | 
				
			||||||
        body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
 | 
					        Accept: "application/json",
 | 
				
			||||||
        headers: new Headers({
 | 
					        "Content-Type": "application/json",
 | 
				
			||||||
          Accept: ["application/json"],
 | 
					      }),
 | 
				
			||||||
          "Content-Type": ["application/json"],
 | 
					      method: "POST",
 | 
				
			||||||
        }),
 | 
					    });
 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    expect(localStorage.getItem("base_url")).toEqual("https://example.com");
 | 
					    expect(localStorage.getItem("base_url")).toEqual("https://example.com");
 | 
				
			||||||
    expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
 | 
					    expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
 | 
				
			||||||
    expect(localStorage.getItem("access_token")).toEqual("foobar");
 | 
					    expect(localStorage.getItem("access_token")).toEqual("foobar");
 | 
				
			||||||
| 
						 | 
					@ -79,14 +77,14 @@ describe("authProvider", () => {
 | 
				
			||||||
    it("should remove the access_token from localStorage", async () => {
 | 
					    it("should remove the access_token from localStorage", async () => {
 | 
				
			||||||
      localStorage.setItem("base_url", "example.com");
 | 
					      localStorage.setItem("base_url", "example.com");
 | 
				
			||||||
      localStorage.setItem("access_token", "foo");
 | 
					      localStorage.setItem("access_token", "foo");
 | 
				
			||||||
      fetch.mockResponse(JSON.stringify({}));
 | 
					      fetchMock.mockResponse(JSON.stringify({}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await authProvider.logout();
 | 
					      await authProvider.logout(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
 | 
					      expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
 | 
				
			||||||
        headers: new Headers({
 | 
					        headers: new Headers({
 | 
				
			||||||
          Accept: ["application/json"],
 | 
					          Accept: "application/json",
 | 
				
			||||||
          Authorization: ["Bearer foo"],
 | 
					          Authorization: "Bearer foo",
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        user: { authenticated: true, token: "Bearer foo" },
 | 
					        user: { authenticated: true, token: "Bearer foo" },
 | 
				
			||||||
| 
						 | 
					@ -97,21 +95,15 @@ describe("authProvider", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe("checkError", () => {
 | 
					  describe("checkError", () => {
 | 
				
			||||||
    it("should resolve if error.status is not 401 or 403", async () => {
 | 
					    it("should resolve if error.status is not 401 or 403", async () => {
 | 
				
			||||||
      await expect(
 | 
					      await expect(authProvider.checkError({ status: 200 })).resolves.toBeUndefined();
 | 
				
			||||||
        authProvider.checkError({ status: 200 })
 | 
					 | 
				
			||||||
      ).resolves.toBeUndefined();
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it("should reject if error.status is 401", async () => {
 | 
					    it("should reject if error.status is 401", async () => {
 | 
				
			||||||
      await expect(
 | 
					      await expect(authProvider.checkError({ status: 401 })).rejects.toBeUndefined();
 | 
				
			||||||
        authProvider.checkError({ status: 401 })
 | 
					 | 
				
			||||||
      ).rejects.toBeUndefined();
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it("should reject if error.status is 403", async () => {
 | 
					    it("should reject if error.status is 403", async () => {
 | 
				
			||||||
      await expect(
 | 
					      await expect(authProvider.checkError({ status: 403 })).rejects.toBeUndefined();
 | 
				
			||||||
        authProvider.checkError({ status: 403 })
 | 
					 | 
				
			||||||
      ).rejects.toBeUndefined();
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -129,7 +121,7 @@ describe("authProvider", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe("getPermissions", () => {
 | 
					  describe("getPermissions", () => {
 | 
				
			||||||
    it("should do nothing", async () => {
 | 
					    it("should do nothing", async () => {
 | 
				
			||||||
      await expect(authProvider.getPermissions()).resolves.toBeUndefined();
 | 
					      await expect(authProvider.getPermissions(null)).resolves.toBeUndefined();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,20 @@
 | 
				
			||||||
import { fetchUtils } from "react-admin";
 | 
					import { AuthProvider, Options, fetchUtils } from "react-admin";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const authProvider = {
 | 
					const authProvider: AuthProvider = {
 | 
				
			||||||
  // called when the user attempts to log in
 | 
					  // called when the user attempts to log in
 | 
				
			||||||
  login: async ({ base_url, username, password, loginToken }) => {
 | 
					  login: async ({
 | 
				
			||||||
 | 
					    base_url,
 | 
				
			||||||
 | 
					    username,
 | 
				
			||||||
 | 
					    password,
 | 
				
			||||||
 | 
					    loginToken,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    base_url: string;
 | 
				
			||||||
 | 
					    username: string;
 | 
				
			||||||
 | 
					    password: string;
 | 
				
			||||||
 | 
					    loginToken: string;
 | 
				
			||||||
 | 
					  }) => {
 | 
				
			||||||
    console.log("login ");
 | 
					    console.log("login ");
 | 
				
			||||||
    const options = {
 | 
					    const options: Options = {
 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
      body: JSON.stringify(
 | 
					      body: JSON.stringify(
 | 
				
			||||||
        Object.assign(
 | 
					        Object.assign(
 | 
				
			||||||
| 
						 | 
					@ -45,11 +55,10 @@ const authProvider = {
 | 
				
			||||||
  logout: async () => {
 | 
					  logout: async () => {
 | 
				
			||||||
    console.log("logout");
 | 
					    console.log("logout");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const logout_api_url =
 | 
					    const logout_api_url = localStorage.getItem("base_url") + "/_matrix/client/r0/logout";
 | 
				
			||||||
      localStorage.getItem("base_url") + "/_matrix/client/r0/logout";
 | 
					 | 
				
			||||||
    const access_token = localStorage.getItem("access_token");
 | 
					    const access_token = localStorage.getItem("access_token");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const options = {
 | 
					    const options: Options = {
 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
      user: {
 | 
					      user: {
 | 
				
			||||||
        authenticated: true,
 | 
					        authenticated: true,
 | 
				
			||||||
| 
						 | 
					@ -63,7 +72,7 @@ const authProvider = {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  // called when the API returns an error
 | 
					  // called when the API returns an error
 | 
				
			||||||
  checkError: ({ status }) => {
 | 
					  checkError: ({ status }: { status: number }) => {
 | 
				
			||||||
    console.log("checkError " + status);
 | 
					    console.log("checkError " + status);
 | 
				
			||||||
    if (status === 401 || status === 403) {
 | 
					    if (status === 401 || status === 403) {
 | 
				
			||||||
      return Promise.reject();
 | 
					      return Promise.reject();
 | 
				
			||||||
| 
						 | 
					@ -74,9 +83,7 @@ const authProvider = {
 | 
				
			||||||
  checkAuth: () => {
 | 
					  checkAuth: () => {
 | 
				
			||||||
    const access_token = localStorage.getItem("access_token");
 | 
					    const access_token = localStorage.getItem("access_token");
 | 
				
			||||||
    console.log("checkAuth " + access_token);
 | 
					    console.log("checkAuth " + access_token);
 | 
				
			||||||
    return typeof access_token === "string"
 | 
					    return typeof access_token === "string" ? Promise.resolve() : Promise.reject();
 | 
				
			||||||
      ? Promise.resolve()
 | 
					 | 
				
			||||||
      : Promise.reject();
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  // called when the user navigates to a new location, to check for permissions / roles
 | 
					  // called when the user navigates to a new location, to check for permissions / roles
 | 
				
			||||||
  getPermissions: () => Promise.resolve(),
 | 
					  getPermissions: () => Promise.resolve(),
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,11 @@
 | 
				
			||||||
 | 
					import fetchMock from "jest-fetch-mock";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import dataProvider from "./dataProvider";
 | 
					import dataProvider from "./dataProvider";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fetchMock.enableMocks();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
beforeEach(() => {
 | 
					beforeEach(() => {
 | 
				
			||||||
  fetch.resetMocks();
 | 
					  fetchMock.resetMocks();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe("dataProvider", () => {
 | 
					describe("dataProvider", () => {
 | 
				
			||||||
| 
						 | 
					@ -9,7 +13,7 @@ describe("dataProvider", () => {
 | 
				
			||||||
  localStorage.setItem("access_token", "access_token");
 | 
					  localStorage.setItem("access_token", "access_token");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it("fetches all users", async () => {
 | 
					  it("fetches all users", async () => {
 | 
				
			||||||
    fetch.mockResponseOnce(
 | 
					    fetchMock.mockResponseOnce(
 | 
				
			||||||
      JSON.stringify({
 | 
					      JSON.stringify({
 | 
				
			||||||
        users: [
 | 
					        users: [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
| 
						 | 
					@ -42,13 +46,13 @@ describe("dataProvider", () => {
 | 
				
			||||||
      filter: { author_id: 12 },
 | 
					      filter: { author_id: 12 },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(users["data"][0]["id"]).toEqual("user_id1");
 | 
					    expect(users.data[0].id).toEqual("user_id1");
 | 
				
			||||||
    expect(users["total"]).toEqual(200);
 | 
					    expect(users.total).toEqual(200);
 | 
				
			||||||
    expect(fetch).toHaveBeenCalledTimes(1);
 | 
					    expect(fetch).toHaveBeenCalledTimes(1);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it("fetches one user", async () => {
 | 
					  it("fetches one user", async () => {
 | 
				
			||||||
    fetch.mockResponseOnce(
 | 
					    fetchMock.mockResponseOnce(
 | 
				
			||||||
      JSON.stringify({
 | 
					      JSON.stringify({
 | 
				
			||||||
        name: "user_id1",
 | 
					        name: "user_id1",
 | 
				
			||||||
        password: "user_password",
 | 
					        password: "user_password",
 | 
				
			||||||
| 
						 | 
					@ -71,8 +75,8 @@ describe("dataProvider", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const user = await dataProvider.getOne("users", { id: "user_id1" });
 | 
					    const user = await dataProvider.getOne("users", { id: "user_id1" });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(user["data"]["id"]).toEqual("user_id1");
 | 
					    expect(user.data.id).toEqual("user_id1");
 | 
				
			||||||
    expect(user["data"]["displayname"]).toEqual("User");
 | 
					    expect(user.data.displayname).toEqual("User");
 | 
				
			||||||
    expect(fetch).toHaveBeenCalledTimes(1);
 | 
					    expect(fetch).toHaveBeenCalledTimes(1);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,9 @@
 | 
				
			||||||
import { fetchUtils } from "react-admin";
 | 
					 | 
				
			||||||
import { stringify } from "query-string";
 | 
					import { stringify } from "query-string";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { DataProvider, DeleteParams, Identifier, Options, RaRecord, fetchUtils } from "react-admin";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Adds the access token to all requests
 | 
					// Adds the access token to all requests
 | 
				
			||||||
const jsonClient = (url, options = {}) => {
 | 
					const jsonClient = (url: string, options: Options = {}) => {
 | 
				
			||||||
  const token = localStorage.getItem("access_token");
 | 
					  const token = localStorage.getItem("access_token");
 | 
				
			||||||
  console.log("httpClient " + url);
 | 
					  console.log("httpClient " + url);
 | 
				
			||||||
  if (token != null) {
 | 
					  if (token != null) {
 | 
				
			||||||
| 
						 | 
					@ -14,10 +15,10 @@ const jsonClient = (url, options = {}) => {
 | 
				
			||||||
  return fetchUtils.fetchJson(url, options);
 | 
					  return fetchUtils.fetchJson(url, options);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mxcUrlToHttp = mxcUrl => {
 | 
					const mxcUrlToHttp = (mxcUrl: string) => {
 | 
				
			||||||
  const homeserver = localStorage.getItem("base_url");
 | 
					  const homeserver = localStorage.getItem("base_url");
 | 
				
			||||||
  const re = /^mxc:\/\/([^/]+)\/(\w+)/;
 | 
					  const re = /^mxc:\/\/([^/]+)\/(\w+)/;
 | 
				
			||||||
  var ret = re.exec(mxcUrl);
 | 
					  const ret = re.exec(mxcUrl);
 | 
				
			||||||
  console.log("mxcClient " + ret);
 | 
					  console.log("mxcClient " + ret);
 | 
				
			||||||
  if (ret == null) return null;
 | 
					  if (ret == null) return null;
 | 
				
			||||||
  const serverName = ret[1];
 | 
					  const serverName = ret[1];
 | 
				
			||||||
| 
						 | 
					@ -25,13 +26,188 @@ const mxcUrlToHttp = mxcUrl => {
 | 
				
			||||||
  return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
 | 
					  return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Room {
 | 
				
			||||||
 | 
					  room_id: string;
 | 
				
			||||||
 | 
					  name?: string;
 | 
				
			||||||
 | 
					  canonical_alias?: string;
 | 
				
			||||||
 | 
					  avatar_url?: string;
 | 
				
			||||||
 | 
					  joined_members: number;
 | 
				
			||||||
 | 
					  joined_local_members: number;
 | 
				
			||||||
 | 
					  version: number;
 | 
				
			||||||
 | 
					  creator: string;
 | 
				
			||||||
 | 
					  encryption?: string;
 | 
				
			||||||
 | 
					  federatable: boolean;
 | 
				
			||||||
 | 
					  public: boolean;
 | 
				
			||||||
 | 
					  join_rules: "public" | "knock" | "invite" | "private";
 | 
				
			||||||
 | 
					  guest_access?: "can_join" | "forbidden";
 | 
				
			||||||
 | 
					  history_visibility: "invited" | "joined" | "shared" | "world_readable";
 | 
				
			||||||
 | 
					  state_events: number;
 | 
				
			||||||
 | 
					  room_type?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RoomState {
 | 
				
			||||||
 | 
					  age: number;
 | 
				
			||||||
 | 
					  content: {
 | 
				
			||||||
 | 
					    alias?: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  event_id: string;
 | 
				
			||||||
 | 
					  origin_server_ts: number;
 | 
				
			||||||
 | 
					  room_id: string;
 | 
				
			||||||
 | 
					  sender: string;
 | 
				
			||||||
 | 
					  state_key: string;
 | 
				
			||||||
 | 
					  type: string;
 | 
				
			||||||
 | 
					  user_id: string;
 | 
				
			||||||
 | 
					  unsigned: {
 | 
				
			||||||
 | 
					    age?: number;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ForwardExtremity {
 | 
				
			||||||
 | 
					  event_id: string;
 | 
				
			||||||
 | 
					  state_group: number;
 | 
				
			||||||
 | 
					  depth: number;
 | 
				
			||||||
 | 
					  received_ts: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface EventReport {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  received_ts: number;
 | 
				
			||||||
 | 
					  room_id: string;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  event_id: string;
 | 
				
			||||||
 | 
					  user_id: string;
 | 
				
			||||||
 | 
					  reason?: string;
 | 
				
			||||||
 | 
					  score?: number;
 | 
				
			||||||
 | 
					  sender: string;
 | 
				
			||||||
 | 
					  canonical_alias?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Threepid {
 | 
				
			||||||
 | 
					  medium: string;
 | 
				
			||||||
 | 
					  address: string;
 | 
				
			||||||
 | 
					  added_at: number;
 | 
				
			||||||
 | 
					  validated_at: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ExternalId {
 | 
				
			||||||
 | 
					  auth_provider: string;
 | 
				
			||||||
 | 
					  external_id: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface User {
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  displayname?: string;
 | 
				
			||||||
 | 
					  threepids: Threepid[];
 | 
				
			||||||
 | 
					  avatar_url?: string;
 | 
				
			||||||
 | 
					  is_guest: 0 | 1;
 | 
				
			||||||
 | 
					  admin: 0 | 1;
 | 
				
			||||||
 | 
					  deactivated: 0 | 1;
 | 
				
			||||||
 | 
					  erased: boolean;
 | 
				
			||||||
 | 
					  shadow_banned: 0 | 1;
 | 
				
			||||||
 | 
					  creation_ts: number;
 | 
				
			||||||
 | 
					  appservice_id?: string;
 | 
				
			||||||
 | 
					  consent_server_notice_sent?: string;
 | 
				
			||||||
 | 
					  consent_version?: string;
 | 
				
			||||||
 | 
					  consent_ts?: number;
 | 
				
			||||||
 | 
					  external_ids: ExternalId[];
 | 
				
			||||||
 | 
					  user_type?: string;
 | 
				
			||||||
 | 
					  locked: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Device {
 | 
				
			||||||
 | 
					  device_id: string;
 | 
				
			||||||
 | 
					  display_name?: string;
 | 
				
			||||||
 | 
					  last_seen_ip?: string;
 | 
				
			||||||
 | 
					  last_seen_user_agent?: string;
 | 
				
			||||||
 | 
					  last_seen_ts?: number;
 | 
				
			||||||
 | 
					  user_id: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Connection {
 | 
				
			||||||
 | 
					  ip: string;
 | 
				
			||||||
 | 
					  last_seen: number;
 | 
				
			||||||
 | 
					  user_agent: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Whois {
 | 
				
			||||||
 | 
					  user_id: string;
 | 
				
			||||||
 | 
					  devices: Record<
 | 
				
			||||||
 | 
					    string,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      sessions: {
 | 
				
			||||||
 | 
					        connections: Connection[];
 | 
				
			||||||
 | 
					      }[];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  >;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Pusher {
 | 
				
			||||||
 | 
					  app_display_name: string;
 | 
				
			||||||
 | 
					  app_id: string;
 | 
				
			||||||
 | 
					  data: {
 | 
				
			||||||
 | 
					    url?: string;
 | 
				
			||||||
 | 
					    format: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  url: string;
 | 
				
			||||||
 | 
					  format: string;
 | 
				
			||||||
 | 
					  device_display_name: string;
 | 
				
			||||||
 | 
					  profile_tag: string;
 | 
				
			||||||
 | 
					  kind: string;
 | 
				
			||||||
 | 
					  lang: string;
 | 
				
			||||||
 | 
					  pushkey: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface UserMedia {
 | 
				
			||||||
 | 
					  created_ts: number;
 | 
				
			||||||
 | 
					  last_access_ts?: number;
 | 
				
			||||||
 | 
					  media_id: string;
 | 
				
			||||||
 | 
					  media_length: number;
 | 
				
			||||||
 | 
					  media_type: string;
 | 
				
			||||||
 | 
					  quarantined_by?: string;
 | 
				
			||||||
 | 
					  safe_from_quarantine: boolean;
 | 
				
			||||||
 | 
					  upload_name?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface UserMediaStatistic {
 | 
				
			||||||
 | 
					  displayname: string;
 | 
				
			||||||
 | 
					  media_count: number;
 | 
				
			||||||
 | 
					  media_length: number;
 | 
				
			||||||
 | 
					  user_id: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RegistrationToken {
 | 
				
			||||||
 | 
					  token: string;
 | 
				
			||||||
 | 
					  uses_allowed: number;
 | 
				
			||||||
 | 
					  pending: number;
 | 
				
			||||||
 | 
					  completed: number;
 | 
				
			||||||
 | 
					  expiry_time?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RaServerNotice {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  body: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Destination {
 | 
				
			||||||
 | 
					  destination: string;
 | 
				
			||||||
 | 
					  retry_last_ts: number;
 | 
				
			||||||
 | 
					  retry_interval: number;
 | 
				
			||||||
 | 
					  failure_ts: number;
 | 
				
			||||||
 | 
					  last_successful_stream_ordering?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface DestinationRoom {
 | 
				
			||||||
 | 
					  room_id: string;
 | 
				
			||||||
 | 
					  stream_ordering: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const resourceMap = {
 | 
					const resourceMap = {
 | 
				
			||||||
  users: {
 | 
					  users: {
 | 
				
			||||||
    path: "/_synapse/admin/v2/users",
 | 
					    path: "/_synapse/admin/v2/users",
 | 
				
			||||||
    map: u => ({
 | 
					    map: (u: User) => ({
 | 
				
			||||||
      ...u,
 | 
					      ...u,
 | 
				
			||||||
      id: u.name,
 | 
					      id: u.name,
 | 
				
			||||||
      avatar_src: mxcUrlToHttp(u.avatar_url),
 | 
					      avatar_src: u.avatar_url ? mxcUrlToHttp(u.avatar_url) : undefined,
 | 
				
			||||||
      is_guest: !!u.is_guest,
 | 
					      is_guest: !!u.is_guest,
 | 
				
			||||||
      admin: !!u.admin,
 | 
					      admin: !!u.admin,
 | 
				
			||||||
      deactivated: !!u.deactivated,
 | 
					      deactivated: !!u.deactivated,
 | 
				
			||||||
| 
						 | 
					@ -40,24 +216,20 @@ const resourceMap = {
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "users",
 | 
					    data: "users",
 | 
				
			||||||
    total: json => json.total,
 | 
					    total: json => json.total,
 | 
				
			||||||
    create: data => ({
 | 
					    create: (data: RaRecord) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(
 | 
					      endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(data.id)}:${localStorage.getItem("home_server")}`,
 | 
				
			||||||
        data.id
 | 
					 | 
				
			||||||
      )}:${localStorage.getItem("home_server")}`,
 | 
					 | 
				
			||||||
      body: data,
 | 
					      body: data,
 | 
				
			||||||
      method: "PUT",
 | 
					      method: "PUT",
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    delete: params => ({
 | 
					    delete: (params: DeleteParams) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(
 | 
					      endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(params.id)}`,
 | 
				
			||||||
        params.id
 | 
					 | 
				
			||||||
      )}`,
 | 
					 | 
				
			||||||
      body: { erase: true },
 | 
					      body: { erase: true },
 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  rooms: {
 | 
					  rooms: {
 | 
				
			||||||
    path: "/_synapse/admin/v1/rooms",
 | 
					    path: "/_synapse/admin/v1/rooms",
 | 
				
			||||||
    map: r => ({
 | 
					    map: (r: Room) => ({
 | 
				
			||||||
      ...r,
 | 
					      ...r,
 | 
				
			||||||
      id: r.room_id,
 | 
					      id: r.room_id,
 | 
				
			||||||
      alias: r.canonical_alias,
 | 
					      alias: r.canonical_alias,
 | 
				
			||||||
| 
						 | 
					@ -67,121 +239,98 @@ const resourceMap = {
 | 
				
			||||||
      public: !!r.public,
 | 
					      public: !!r.public,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "rooms",
 | 
					    data: "rooms",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.total_rooms,
 | 
				
			||||||
      return json.total_rooms;
 | 
					    delete: (params: DeleteParams) => ({
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    delete: params => ({
 | 
					 | 
				
			||||||
      endpoint: `/_synapse/admin/v2/rooms/${params.id}`,
 | 
					      endpoint: `/_synapse/admin/v2/rooms/${params.id}`,
 | 
				
			||||||
      body: { block: false },
 | 
					      body: { block: false },
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  reports: {
 | 
					  reports: {
 | 
				
			||||||
    path: "/_synapse/admin/v1/event_reports",
 | 
					    path: "/_synapse/admin/v1/event_reports",
 | 
				
			||||||
    map: er => ({
 | 
					    map: (er: EventReport) => ({ ...er }),
 | 
				
			||||||
      ...er,
 | 
					 | 
				
			||||||
      id: er.id,
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
    data: "event_reports",
 | 
					    data: "event_reports",
 | 
				
			||||||
    total: json => json.total,
 | 
					    total: json => json.total,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  devices: {
 | 
					  devices: {
 | 
				
			||||||
    map: d => ({
 | 
					    map: (d: Device) => ({
 | 
				
			||||||
      ...d,
 | 
					      ...d,
 | 
				
			||||||
      id: d.device_id,
 | 
					      id: d.device_id,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "devices",
 | 
					    data: "devices",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.total,
 | 
				
			||||||
      return json.total;
 | 
					    reference: (id: Identifier) => ({
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    reference: id => ({
 | 
					 | 
				
			||||||
      endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(id)}/devices`,
 | 
					      endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(id)}/devices`,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    delete: params => ({
 | 
					    delete: (params: DeleteParams) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
 | 
					      endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(params.previousData.user_id)}/devices/${params.id}`,
 | 
				
			||||||
        params.previousData.user_id
 | 
					 | 
				
			||||||
      )}/devices/${params.id}`,
 | 
					 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  connections: {
 | 
					  connections: {
 | 
				
			||||||
    path: "/_synapse/admin/v1/whois",
 | 
					    path: "/_synapse/admin/v1/whois",
 | 
				
			||||||
    map: c => ({
 | 
					    map: (c: Whois) => ({
 | 
				
			||||||
      ...c,
 | 
					      ...c,
 | 
				
			||||||
      id: c.user_id,
 | 
					      id: c.user_id,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "connections",
 | 
					    data: "connections",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  room_members: {
 | 
					  room_members: {
 | 
				
			||||||
    map: m => ({
 | 
					    map: (m: string) => ({
 | 
				
			||||||
      id: m,
 | 
					      id: m,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    reference: id => ({
 | 
					    reference: (id: Identifier) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/rooms/${id}/members`,
 | 
					      endpoint: `/_synapse/admin/v1/rooms/${id}/members`,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "members",
 | 
					    data: "members",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.total,
 | 
				
			||||||
      return json.total;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  room_state: {
 | 
					  room_state: {
 | 
				
			||||||
    map: rs => ({
 | 
					    map: (rs: RoomState) => ({
 | 
				
			||||||
      ...rs,
 | 
					      ...rs,
 | 
				
			||||||
      id: rs.event_id,
 | 
					      id: rs.event_id,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    reference: id => ({
 | 
					    reference: (id: Identifier) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/rooms/${id}/state`,
 | 
					      endpoint: `/_synapse/admin/v1/rooms/${id}/state`,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "state",
 | 
					    data: "state",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.state.length,
 | 
				
			||||||
      return json.state.length;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  pushers: {
 | 
					  pushers: {
 | 
				
			||||||
    map: p => ({
 | 
					    map: (p: Pusher) => ({
 | 
				
			||||||
      ...p,
 | 
					      ...p,
 | 
				
			||||||
      id: p.pushkey,
 | 
					      id: p.pushkey,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    reference: id => ({
 | 
					    reference: (id: Identifier) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/pushers`,
 | 
					      endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/pushers`,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "pushers",
 | 
					    data: "pushers",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.total,
 | 
				
			||||||
      return json.total;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  joined_rooms: {
 | 
					  joined_rooms: {
 | 
				
			||||||
    map: jr => ({
 | 
					    map: (jr: string) => ({
 | 
				
			||||||
      id: jr,
 | 
					      id: jr,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    reference: id => ({
 | 
					    reference: (id: Identifier) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(
 | 
					      endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/joined_rooms`,
 | 
				
			||||||
        id
 | 
					 | 
				
			||||||
      )}/joined_rooms`,
 | 
					 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "joined_rooms",
 | 
					    data: "joined_rooms",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.total,
 | 
				
			||||||
      return json.total;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  users_media: {
 | 
					  users_media: {
 | 
				
			||||||
    map: um => ({
 | 
					    map: (um: UserMedia) => ({
 | 
				
			||||||
      ...um,
 | 
					      ...um,
 | 
				
			||||||
      id: um.media_id,
 | 
					      id: um.media_id,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    reference: id => ({
 | 
					    reference: (id: Identifier) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`,
 | 
					      endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "media",
 | 
					    data: "media",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.total,
 | 
				
			||||||
      return json.total;
 | 
					    delete: (params: DeleteParams) => ({
 | 
				
			||||||
    },
 | 
					      endpoint: `/_synapse/admin/v1/media/${localStorage.getItem("home_server")}/${params.id}`,
 | 
				
			||||||
    delete: params => ({
 | 
					 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
 | 
					 | 
				
			||||||
        "home_server"
 | 
					 | 
				
			||||||
      )}/${params.id}`,
 | 
					 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  delete_media: {
 | 
					  delete_media: {
 | 
				
			||||||
    delete: params => ({
 | 
					    delete: (params: DeleteParams) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
 | 
					      endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
 | 
				
			||||||
        "home_server"
 | 
					        "home_server"
 | 
				
			||||||
      )}/delete?before_ts=${params.meta.before_ts}&size_gt=${
 | 
					      )}/delete?before_ts=${params.meta.before_ts}&size_gt=${
 | 
				
			||||||
| 
						 | 
					@ -191,34 +340,30 @@ const resourceMap = {
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  protect_media: {
 | 
					  protect_media: {
 | 
				
			||||||
    map: pm => ({ id: pm.media_id }),
 | 
					    map: (pm: UserMedia) => ({ id: pm.media_id }),
 | 
				
			||||||
    create: params => ({
 | 
					    create: (params: UserMedia) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/media/protect/${params.media_id}`,
 | 
					      endpoint: `/_synapse/admin/v1/media/protect/${params.media_id}`,
 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    delete: params => ({
 | 
					    delete: (params: DeleteParams) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/media/unprotect/${params.id}`,
 | 
					      endpoint: `/_synapse/admin/v1/media/unprotect/${params.id}`,
 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  quarantine_media: {
 | 
					  quarantine_media: {
 | 
				
			||||||
    map: qm => ({ id: qm.media_id }),
 | 
					    map: (qm: UserMedia) => ({ id: qm.media_id }),
 | 
				
			||||||
    create: params => ({
 | 
					    create: (params: UserMedia) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem(
 | 
					      endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem("home_server")}/${params.media_id}`,
 | 
				
			||||||
        "home_server"
 | 
					 | 
				
			||||||
      )}/${params.media_id}`,
 | 
					 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    delete: params => ({
 | 
					    delete: (params: DeleteParams) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem(
 | 
					      endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem("home_server")}/${params.id}`,
 | 
				
			||||||
        "home_server"
 | 
					 | 
				
			||||||
      )}/${params.id}`,
 | 
					 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  servernotices: {
 | 
					  servernotices: {
 | 
				
			||||||
    map: n => ({ id: n.event_id }),
 | 
					    map: (n: { event_id: string }) => ({ id: n.event_id }),
 | 
				
			||||||
    create: data => ({
 | 
					    create: (data: RaServerNotice) => ({
 | 
				
			||||||
      endpoint: "/_synapse/admin/v1/send_server_notice",
 | 
					      endpoint: "/_synapse/admin/v1/send_server_notice",
 | 
				
			||||||
      body: {
 | 
					      body: {
 | 
				
			||||||
        user_id: data.id,
 | 
					        user_id: data.id,
 | 
				
			||||||
| 
						 | 
					@ -232,50 +377,44 @@ const resourceMap = {
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  user_media_statistics: {
 | 
					  user_media_statistics: {
 | 
				
			||||||
    path: "/_synapse/admin/v1/statistics/users/media",
 | 
					    path: "/_synapse/admin/v1/statistics/users/media",
 | 
				
			||||||
    map: usms => ({
 | 
					    map: (usms: UserMediaStatistic) => ({
 | 
				
			||||||
      ...usms,
 | 
					      ...usms,
 | 
				
			||||||
      id: usms.user_id,
 | 
					      id: usms.user_id,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "users",
 | 
					    data: "users",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.total,
 | 
				
			||||||
      return json.total;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  forward_extremities: {
 | 
					  forward_extremities: {
 | 
				
			||||||
    map: fe => ({
 | 
					    map: (fe: ForwardExtremity) => ({
 | 
				
			||||||
      ...fe,
 | 
					      ...fe,
 | 
				
			||||||
      id: fe.event_id,
 | 
					      id: fe.event_id,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    reference: id => ({
 | 
					    reference: (id: Identifier) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/rooms/${id}/forward_extremities`,
 | 
					      endpoint: `/_synapse/admin/v1/rooms/${id}/forward_extremities`,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "results",
 | 
					    data: "results",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.count,
 | 
				
			||||||
      return json.count;
 | 
					    delete: (params: DeleteParams) => ({
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    delete: params => ({
 | 
					 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/rooms/${params.id}/forward_extremities`,
 | 
					      endpoint: `/_synapse/admin/v1/rooms/${params.id}/forward_extremities`,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  room_directory: {
 | 
					  room_directory: {
 | 
				
			||||||
    path: "/_matrix/client/r0/publicRooms",
 | 
					    path: "/_matrix/client/r0/publicRooms",
 | 
				
			||||||
    map: rd => ({
 | 
					    map: (rd: Room) => ({
 | 
				
			||||||
      ...rd,
 | 
					      ...rd,
 | 
				
			||||||
      id: rd.room_id,
 | 
					      id: rd.room_id,
 | 
				
			||||||
      public: !!rd.public,
 | 
					      public: !!rd.public,
 | 
				
			||||||
      guest_access: !!rd.guest_access,
 | 
					      guest_access: !!rd.guest_access,
 | 
				
			||||||
      avatar_src: mxcUrlToHttp(rd.avatar_url),
 | 
					      avatar_src: rd.avatar_url ? mxcUrlToHttp(rd.avatar_url) : undefined,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "chunk",
 | 
					    data: "chunk",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.total_room_count_estimate,
 | 
				
			||||||
      return json.total_room_count_estimate;
 | 
					    create: (params: RaRecord) => ({
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    create: params => ({
 | 
					 | 
				
			||||||
      endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
 | 
					      endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
 | 
				
			||||||
      body: { visibility: "public" },
 | 
					      body: { visibility: "public" },
 | 
				
			||||||
      method: "PUT",
 | 
					      method: "PUT",
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    delete: params => ({
 | 
					    delete: (params: DeleteParams) => ({
 | 
				
			||||||
      endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
 | 
					      endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
 | 
				
			||||||
      body: { visibility: "private" },
 | 
					      body: { visibility: "private" },
 | 
				
			||||||
      method: "PUT",
 | 
					      method: "PUT",
 | 
				
			||||||
| 
						 | 
					@ -283,54 +422,49 @@ const resourceMap = {
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  destinations: {
 | 
					  destinations: {
 | 
				
			||||||
    path: "/_synapse/admin/v1/federation/destinations",
 | 
					    path: "/_synapse/admin/v1/federation/destinations",
 | 
				
			||||||
    map: dst => ({
 | 
					    map: (dst: Destination) => ({
 | 
				
			||||||
      ...dst,
 | 
					      ...dst,
 | 
				
			||||||
      id: dst.destination,
 | 
					      id: dst.destination,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "destinations",
 | 
					    data: "destinations",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.total,
 | 
				
			||||||
      return json.total;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    delete: params => ({
 | 
					    delete: params => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/federation/destinations/${params.id}/reset_connection`,
 | 
					      endpoint: `/_synapse/admin/v1/federation/destinations/${params.id}/reset_connection`,
 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  destination_rooms: {
 | 
					  destination_rooms: {
 | 
				
			||||||
    map: dstroom => ({
 | 
					    map: (dstroom: DestinationRoom) => ({
 | 
				
			||||||
      ...dstroom,
 | 
					      ...dstroom,
 | 
				
			||||||
      id: dstroom.room_id,
 | 
					      id: dstroom.room_id,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    reference: id => ({
 | 
					    reference: (id: Identifier) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/federation/destinations/${id}/rooms`,
 | 
					      endpoint: `/_synapse/admin/v1/federation/destinations/${id}/rooms`,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "rooms",
 | 
					    data: "rooms",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.total,
 | 
				
			||||||
      return json.total;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  registration_tokens: {
 | 
					  registration_tokens: {
 | 
				
			||||||
    path: "/_synapse/admin/v1/registration_tokens",
 | 
					    path: "/_synapse/admin/v1/registration_tokens",
 | 
				
			||||||
    map: rt => ({
 | 
					    map: (rt: RegistrationToken) => ({
 | 
				
			||||||
      ...rt,
 | 
					      ...rt,
 | 
				
			||||||
      id: rt.token,
 | 
					      id: rt.token,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    data: "registration_tokens",
 | 
					    data: "registration_tokens",
 | 
				
			||||||
    total: json => {
 | 
					    total: json => json.registration_tokens.length,
 | 
				
			||||||
      return json.registration_tokens.length;
 | 
					    create: (params: RaRecord) => ({
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    create: params => ({
 | 
					 | 
				
			||||||
      endpoint: "/_synapse/admin/v1/registration_tokens/new",
 | 
					      endpoint: "/_synapse/admin/v1/registration_tokens/new",
 | 
				
			||||||
      body: params,
 | 
					      body: params,
 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    delete: params => ({
 | 
					    delete: (params: DeleteParams) => ({
 | 
				
			||||||
      endpoint: `/_synapse/admin/v1/registration_tokens/${params.id}`,
 | 
					      endpoint: `/_synapse/admin/v1/registration_tokens/${params.id}`,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function filterNullValues(key, value) {
 | 
					/* eslint-disable  @typescript-eslint/no-explicit-any */
 | 
				
			||||||
 | 
					function filterNullValues(key: string, value: any) {
 | 
				
			||||||
  // Filtering out null properties
 | 
					  // Filtering out null properties
 | 
				
			||||||
  // to reset user_type from user, it must be null
 | 
					  // to reset user_type from user, it must be null
 | 
				
			||||||
  if (value === null && key !== "user_type") {
 | 
					  if (value === null && key !== "user_type") {
 | 
				
			||||||
| 
						 | 
					@ -339,7 +473,7 @@ function filterNullValues(key, value) {
 | 
				
			||||||
  return value;
 | 
					  return value;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getSearchOrder(order) {
 | 
					function getSearchOrder(order: "ASC" | "DESC") {
 | 
				
			||||||
  if (order === "DESC") {
 | 
					  if (order === "DESC") {
 | 
				
			||||||
    return "b";
 | 
					    return "b";
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
| 
						 | 
					@ -347,18 +481,10 @@ function getSearchOrder(order) {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const dataProvider = {
 | 
					const dataProvider: DataProvider = {
 | 
				
			||||||
  getList: async (resource, params) => {
 | 
					  getList: async (resource, params) => {
 | 
				
			||||||
    console.log("getList " + resource);
 | 
					    console.log("getList " + resource);
 | 
				
			||||||
    const {
 | 
					    const { user_id, name, guests, deactivated, search_term, destination, valid } = params.filter;
 | 
				
			||||||
      user_id,
 | 
					 | 
				
			||||||
      name,
 | 
					 | 
				
			||||||
      guests,
 | 
					 | 
				
			||||||
      deactivated,
 | 
					 | 
				
			||||||
      search_term,
 | 
					 | 
				
			||||||
      destination,
 | 
					 | 
				
			||||||
      valid,
 | 
					 | 
				
			||||||
    } = params.filter;
 | 
					 | 
				
			||||||
    const { page, perPage } = params.pagination;
 | 
					    const { page, perPage } = params.pagination;
 | 
				
			||||||
    const { field, order } = params.sort;
 | 
					    const { field, order } = params.sort;
 | 
				
			||||||
    const from = (page - 1) * perPage;
 | 
					    const from = (page - 1) * perPage;
 | 
				
			||||||
| 
						 | 
					@ -376,7 +502,7 @@ const dataProvider = {
 | 
				
			||||||
      dir: getSearchOrder(order),
 | 
					      dir: getSearchOrder(order),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    const homeserver = localStorage.getItem("base_url");
 | 
					    const homeserver = localStorage.getItem("base_url");
 | 
				
			||||||
    if (!homeserver || !(resource in resourceMap)) return Promise.reject();
 | 
					    if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = resourceMap[resource];
 | 
					    const res = resourceMap[resource];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -393,30 +519,24 @@ const dataProvider = {
 | 
				
			||||||
  getOne: async (resource, params) => {
 | 
					  getOne: async (resource, params) => {
 | 
				
			||||||
    console.log("getOne " + resource);
 | 
					    console.log("getOne " + resource);
 | 
				
			||||||
    const homeserver = localStorage.getItem("base_url");
 | 
					    const homeserver = localStorage.getItem("base_url");
 | 
				
			||||||
    if (!homeserver || !(resource in resourceMap)) return Promise.reject();
 | 
					    if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = resourceMap[resource];
 | 
					    const res = resourceMap[resource];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const endpoint_url = homeserver + res.path;
 | 
					    const endpoint_url = homeserver + res.path;
 | 
				
			||||||
    const { json } = await jsonClient(
 | 
					    const { json } = await jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`);
 | 
				
			||||||
      `${endpoint_url}/${encodeURIComponent(params.id)}`
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    return { data: res.map(json) };
 | 
					    return { data: res.map(json) };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getMany: async (resource, params) => {
 | 
					  getMany: async (resource, params) => {
 | 
				
			||||||
    console.log("getMany " + resource);
 | 
					    console.log("getMany " + resource);
 | 
				
			||||||
    const homeserver = localStorage.getItem("base_url");
 | 
					    const homeserver = localStorage.getItem("base_url");
 | 
				
			||||||
    if (!homeserver || !(resource in resourceMap)) return Promise.reject();
 | 
					    if (!homeserver || !(resource in resourceMap)) throw Error("Homerserver not set");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = resourceMap[resource];
 | 
					    const res = resourceMap[resource];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const endpoint_url = homeserver + res.path;
 | 
					    const endpoint_url = homeserver + res.path;
 | 
				
			||||||
    const responses = await Promise.all(
 | 
					    const responses = await Promise.all(params.ids.map(id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)));
 | 
				
			||||||
      params.ids.map(id =>
 | 
					 | 
				
			||||||
        jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      data: responses.map(({ json }) => res.map(json)),
 | 
					      data: responses.map(({ json }) => res.map(json)),
 | 
				
			||||||
      total: responses.length,
 | 
					      total: responses.length,
 | 
				
			||||||
| 
						 | 
					@ -436,11 +556,11 @@ const dataProvider = {
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const homeserver = localStorage.getItem("base_url");
 | 
					    const homeserver = localStorage.getItem("base_url");
 | 
				
			||||||
    if (!homeserver || !(resource in resourceMap)) return Promise.reject();
 | 
					    if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = resourceMap[resource];
 | 
					    const res = resourceMap[resource];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ref = res["reference"](params.id);
 | 
					    const ref = res.reference(params.id);
 | 
				
			||||||
    const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
 | 
					    const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { json } = await jsonClient(endpoint_url);
 | 
					    const { json } = await jsonClient(endpoint_url);
 | 
				
			||||||
| 
						 | 
					@ -453,37 +573,31 @@ const dataProvider = {
 | 
				
			||||||
  update: async (resource, params) => {
 | 
					  update: async (resource, params) => {
 | 
				
			||||||
    console.log("update " + resource);
 | 
					    console.log("update " + resource);
 | 
				
			||||||
    const homeserver = localStorage.getItem("base_url");
 | 
					    const homeserver = localStorage.getItem("base_url");
 | 
				
			||||||
    if (!homeserver || !(resource in resourceMap)) return Promise.reject();
 | 
					    if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = resourceMap[resource];
 | 
					    const res = resourceMap[resource];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const endpoint_url = homeserver + res.path;
 | 
					    const endpoint_url = homeserver + res.path;
 | 
				
			||||||
    const { json } = await jsonClient(
 | 
					    const { json } = await jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, {
 | 
				
			||||||
      `${endpoint_url}/${encodeURIComponent(params.id)}`,
 | 
					      method: "PUT",
 | 
				
			||||||
      {
 | 
					      body: JSON.stringify(params.data, filterNullValues),
 | 
				
			||||||
        method: "PUT",
 | 
					    });
 | 
				
			||||||
        body: JSON.stringify(params.data, filterNullValues),
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    return { data: res.map(json) };
 | 
					    return { data: res.map(json) };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateMany: async (resource, params) => {
 | 
					  updateMany: async (resource, params) => {
 | 
				
			||||||
    console.log("updateMany " + resource);
 | 
					    console.log("updateMany " + resource);
 | 
				
			||||||
    const homeserver = localStorage.getItem("base_url");
 | 
					    const homeserver = localStorage.getItem("base_url");
 | 
				
			||||||
    if (!homeserver || !(resource in resourceMap)) return Promise.reject();
 | 
					    if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = resourceMap[resource];
 | 
					    const res = resourceMap[resource];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const endpoint_url = homeserver + res.path;
 | 
					    const endpoint_url = homeserver + res.path;
 | 
				
			||||||
    const responses = await Promise.all(
 | 
					    const responses = await Promise.all(
 | 
				
			||||||
      params.ids.map(
 | 
					      params.ids.map(id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`), {
 | 
				
			||||||
        id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
 | 
					        method: "PUT",
 | 
				
			||||||
        {
 | 
					        body: JSON.stringify(params.data, filterNullValues),
 | 
				
			||||||
          method: "PUT",
 | 
					      })
 | 
				
			||||||
          body: JSON.stringify(params.data, filterNullValues),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    return { data: responses.map(({ json }) => json) };
 | 
					    return { data: responses.map(({ json }) => json) };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -491,12 +605,12 @@ const dataProvider = {
 | 
				
			||||||
  create: async (resource, params) => {
 | 
					  create: async (resource, params) => {
 | 
				
			||||||
    console.log("create " + resource);
 | 
					    console.log("create " + resource);
 | 
				
			||||||
    const homeserver = localStorage.getItem("base_url");
 | 
					    const homeserver = localStorage.getItem("base_url");
 | 
				
			||||||
    if (!homeserver || !(resource in resourceMap)) return Promise.reject();
 | 
					    if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = resourceMap[resource];
 | 
					    const res = resourceMap[resource];
 | 
				
			||||||
    if (!("create" in res)) return Promise.reject();
 | 
					    if (!("create" in res)) return Promise.reject();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const create = res["create"](params.data);
 | 
					    const create = res.create(params.data);
 | 
				
			||||||
    const endpoint_url = homeserver + create.endpoint;
 | 
					    const endpoint_url = homeserver + create.endpoint;
 | 
				
			||||||
    const { json } = await jsonClient(endpoint_url, {
 | 
					    const { json } = await jsonClient(endpoint_url, {
 | 
				
			||||||
      method: create.method,
 | 
					      method: create.method,
 | 
				
			||||||
| 
						 | 
					@ -505,18 +619,18 @@ const dataProvider = {
 | 
				
			||||||
    return { data: res.map(json) };
 | 
					    return { data: res.map(json) };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createMany: async (resource, params) => {
 | 
					  createMany: async (resource: string, params: { ids: Identifier[]; data: RaRecord }) => {
 | 
				
			||||||
    console.log("createMany " + resource);
 | 
					    console.log("createMany " + resource);
 | 
				
			||||||
    const homeserver = localStorage.getItem("base_url");
 | 
					    const homeserver = localStorage.getItem("base_url");
 | 
				
			||||||
    if (!homeserver || !(resource in resourceMap)) return Promise.reject();
 | 
					    if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = resourceMap[resource];
 | 
					    const res = resourceMap[resource];
 | 
				
			||||||
    if (!("create" in res)) return Promise.reject();
 | 
					    if (!("create" in res)) throw Error(`Create ${resource} is not allowed`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const responses = await Promise.all(
 | 
					    const responses = await Promise.all(
 | 
				
			||||||
      params.ids.map(id => {
 | 
					      params.ids.map(id => {
 | 
				
			||||||
        params.data.id = id;
 | 
					        params.data.id = id;
 | 
				
			||||||
        const cre = res["create"](params.data);
 | 
					        const cre = res.create(params.data);
 | 
				
			||||||
        const endpoint_url = homeserver + cre.endpoint;
 | 
					        const endpoint_url = homeserver + cre.endpoint;
 | 
				
			||||||
        return jsonClient(endpoint_url, {
 | 
					        return jsonClient(endpoint_url, {
 | 
				
			||||||
          method: cre.method,
 | 
					          method: cre.method,
 | 
				
			||||||
| 
						 | 
					@ -530,12 +644,12 @@ const dataProvider = {
 | 
				
			||||||
  delete: async (resource, params) => {
 | 
					  delete: async (resource, params) => {
 | 
				
			||||||
    console.log("delete " + resource);
 | 
					    console.log("delete " + resource);
 | 
				
			||||||
    const homeserver = localStorage.getItem("base_url");
 | 
					    const homeserver = localStorage.getItem("base_url");
 | 
				
			||||||
    if (!homeserver || !(resource in resourceMap)) return Promise.reject();
 | 
					    if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = resourceMap[resource];
 | 
					    const res = resourceMap[resource];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if ("delete" in res) {
 | 
					    if ("delete" in res) {
 | 
				
			||||||
      const del = res["delete"](params);
 | 
					      const del = res.delete(params);
 | 
				
			||||||
      const endpoint_url = homeserver + del.endpoint;
 | 
					      const endpoint_url = homeserver + del.endpoint;
 | 
				
			||||||
      const { json } = await jsonClient(endpoint_url, {
 | 
					      const { json } = await jsonClient(endpoint_url, {
 | 
				
			||||||
        method: "method" in del ? del.method : "DELETE",
 | 
					        method: "method" in del ? del.method : "DELETE",
 | 
				
			||||||
| 
						 | 
					@ -555,14 +669,14 @@ const dataProvider = {
 | 
				
			||||||
  deleteMany: async (resource, params) => {
 | 
					  deleteMany: async (resource, params) => {
 | 
				
			||||||
    console.log("deleteMany " + resource);
 | 
					    console.log("deleteMany " + resource);
 | 
				
			||||||
    const homeserver = localStorage.getItem("base_url");
 | 
					    const homeserver = localStorage.getItem("base_url");
 | 
				
			||||||
    if (!homeserver || !(resource in resourceMap)) return Promise.reject();
 | 
					    if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = resourceMap[resource];
 | 
					    const res = resourceMap[resource];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if ("delete" in res) {
 | 
					    if ("delete" in res) {
 | 
				
			||||||
      const responses = await Promise.all(
 | 
					      const responses = await Promise.all(
 | 
				
			||||||
        params.ids.map(id => {
 | 
					        params.ids.map(id => {
 | 
				
			||||||
          const del = res["delete"]({ ...params, id: id });
 | 
					          const del = res.delete({ ...params, id: id });
 | 
				
			||||||
          const endpoint_url = homeserver + del.endpoint;
 | 
					          const endpoint_url = homeserver + del.endpoint;
 | 
				
			||||||
          return jsonClient(endpoint_url, {
 | 
					          return jsonClient(endpoint_url, {
 | 
				
			||||||
            method: "method" in del ? del.method : "DELETE",
 | 
					            method: "method" in del ? del.method : "DELETE",
 | 
				
			||||||
| 
						 | 
					@ -579,7 +693,7 @@ const dataProvider = {
 | 
				
			||||||
        params.ids.map(id =>
 | 
					        params.ids.map(id =>
 | 
				
			||||||
          jsonClient(`${endpoint_url}/${id}`, {
 | 
					          jsonClient(`${endpoint_url}/${id}`, {
 | 
				
			||||||
            method: "DELETE",
 | 
					            method: "DELETE",
 | 
				
			||||||
            body: JSON.stringify(params.data, filterNullValues),
 | 
					            // body: JSON.stringify(params.data, filterNullValues),  @FIXME
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
| 
						 | 
					@ -1,31 +0,0 @@
 | 
				
			||||||
import { isValidBaseUrl, splitMxid } from "./synapse";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe("splitMxid", () => {
 | 
					 | 
				
			||||||
  it("splits valid MXIDs", () =>
 | 
					 | 
				
			||||||
    expect(splitMxid("@name:domain.tld")).toEqual({
 | 
					 | 
				
			||||||
      name: "name",
 | 
					 | 
				
			||||||
      domain: "domain.tld",
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
  it("rejects invalid MXIDs", () => expect(splitMxid("foo")).toBeUndefined());
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe("isValidBaseUrl", () => {
 | 
					 | 
				
			||||||
  it("accepts a http URL", () =>
 | 
					 | 
				
			||||||
    expect(isValidBaseUrl("http://foo.bar")).toBeTruthy());
 | 
					 | 
				
			||||||
  it("accepts a https URL", () =>
 | 
					 | 
				
			||||||
    expect(isValidBaseUrl("https://foo.bar")).toBeTruthy());
 | 
					 | 
				
			||||||
  it("accepts a valid URL with port", () =>
 | 
					 | 
				
			||||||
    expect(isValidBaseUrl("https://foo.bar:1234")).toBeTruthy());
 | 
					 | 
				
			||||||
  it("rejects undefined base URLs", () =>
 | 
					 | 
				
			||||||
    expect(isValidBaseUrl(undefined)).toBeFalsy());
 | 
					 | 
				
			||||||
  it("rejects null base URLs", () => expect(isValidBaseUrl(null)).toBeFalsy());
 | 
					 | 
				
			||||||
  it("rejects empty base URLs", () => expect(isValidBaseUrl("")).toBeFalsy());
 | 
					 | 
				
			||||||
  it("rejects non-string base URLs", () =>
 | 
					 | 
				
			||||||
    expect(isValidBaseUrl({})).toBeFalsy());
 | 
					 | 
				
			||||||
  it("rejects base URLs without protocol", () =>
 | 
					 | 
				
			||||||
    expect(isValidBaseUrl("foo.bar")).toBeFalsy());
 | 
					 | 
				
			||||||
  it("rejects base URLs with path", () =>
 | 
					 | 
				
			||||||
    expect(isValidBaseUrl("http://foo.bar/path")).toBeFalsy());
 | 
					 | 
				
			||||||
  it("rejects invalid base URLs", () =>
 | 
					 | 
				
			||||||
    expect(isValidBaseUrl("http:/foo.bar")).toBeFalsy());
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
							
								
								
									
										23
									
								
								src/synapse/synapse.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/synapse/synapse.test.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					import { isValidBaseUrl, splitMxid } from "./synapse";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("splitMxid", () => {
 | 
				
			||||||
 | 
					  it("splits valid MXIDs", () =>
 | 
				
			||||||
 | 
					    expect(splitMxid("@name:domain.tld")).toEqual({
 | 
				
			||||||
 | 
					      name: "name",
 | 
				
			||||||
 | 
					      domain: "domain.tld",
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  it("rejects invalid MXIDs", () => expect(splitMxid("foo")).toBeUndefined());
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("isValidBaseUrl", () => {
 | 
				
			||||||
 | 
					  it("accepts a http URL", () => expect(isValidBaseUrl("http://foo.bar")).toBeTruthy());
 | 
				
			||||||
 | 
					  it("accepts a https URL", () => expect(isValidBaseUrl("https://foo.bar")).toBeTruthy());
 | 
				
			||||||
 | 
					  it("accepts a valid URL with port", () => expect(isValidBaseUrl("https://foo.bar:1234")).toBeTruthy());
 | 
				
			||||||
 | 
					  it("rejects undefined base URLs", () => expect(isValidBaseUrl(undefined)).toBeFalsy());
 | 
				
			||||||
 | 
					  it("rejects null base URLs", () => expect(isValidBaseUrl(null)).toBeFalsy());
 | 
				
			||||||
 | 
					  it("rejects empty base URLs", () => expect(isValidBaseUrl("")).toBeFalsy());
 | 
				
			||||||
 | 
					  it("rejects non-string base URLs", () => expect(isValidBaseUrl({})).toBeFalsy());
 | 
				
			||||||
 | 
					  it("rejects base URLs without protocol", () => expect(isValidBaseUrl("foo.bar")).toBeFalsy());
 | 
				
			||||||
 | 
					  it("rejects base URLs with path", () => expect(isValidBaseUrl("http://foo.bar/path")).toBeFalsy());
 | 
				
			||||||
 | 
					  it("rejects invalid base URLs", () => expect(isValidBaseUrl("http:/foo.bar")).toBeFalsy());
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,11 @@
 | 
				
			||||||
import { fetchUtils } from "react-admin";
 | 
					import { fetchUtils } from "react-admin";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const splitMxid = mxid => {
 | 
					export const splitMxid = mxid => {
 | 
				
			||||||
  const re =
 | 
					  const re = /^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
 | 
				
			||||||
    /^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
 | 
					 | 
				
			||||||
  return re.exec(mxid)?.groups;
 | 
					  return re.exec(mxid)?.groups;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const isValidBaseUrl = baseUrl =>
 | 
					export const isValidBaseUrl = baseUrl => /^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/.test(baseUrl);
 | 
				
			||||||
  /^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/.test(baseUrl);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Resolve the homeserver URL using the well-known lookup
 | 
					 * Resolve the homeserver URL using the well-known lookup
 | 
				
			||||||
| 
						 | 
					@ -58,3 +56,27 @@ export const getMediaUrl = media_id => {
 | 
				
			||||||
  const baseUrl = localStorage.getItem("base_url");
 | 
					  const baseUrl = localStorage.getItem("base_url");
 | 
				
			||||||
  return `${baseUrl}/_matrix/media/v1/download/${media_id}?allow_redirect=true`;
 | 
					  return `${baseUrl}/_matrix/media/v1/download/${media_id}?allow_redirect=true`;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Generate a random MXID for current homeserver
 | 
				
			||||||
 | 
					 * @returns full MXID as string
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function generateRandomMxId(): string {
 | 
				
			||||||
 | 
					  const homeserver = localStorage.getItem("home_server");
 | 
				
			||||||
 | 
					  const characters = "0123456789abcdefghijklmnopqrstuvwxyz";
 | 
				
			||||||
 | 
					  const localpart = Array.from(crypto.getRandomValues(new Uint32Array(8)))
 | 
				
			||||||
 | 
					    .map(x => characters[x % characters.length])
 | 
				
			||||||
 | 
					    .join("");
 | 
				
			||||||
 | 
					  return `@${localpart}:${homeserver}`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Generate a random user password
 | 
				
			||||||
 | 
					 * @returns a new random password as string
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function generateRandomPassword(length = 20): string {
 | 
				
			||||||
 | 
					  const characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$";
 | 
				
			||||||
 | 
					  return Array.from(crypto.getRandomValues(new Uint32Array(length)))
 | 
				
			||||||
 | 
					    .map(x => characters[x % characters.length])
 | 
				
			||||||
 | 
					    .join("");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								tsconfig.eslint.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tsconfig.eslint.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "extends": "./tsconfig.json",
 | 
				
			||||||
 | 
					  "include": ["./**/*.ts", "./**/*.tsx"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										64
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,64 @@
 | 
				
			||||||
 | 
					// prettier-ignore
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    /* Basic Options */
 | 
				
			||||||
 | 
					    "target": "ESNext"                        /* Specify ECMAScript target version */,
 | 
				
			||||||
 | 
					    "module": "ESNext"                        /* Specify module code generation */,
 | 
				
			||||||
 | 
					    "lib": ["DOM", "DOM.Iterable", "ESNext"]  /* Specify library files to be included in the compilation. */,
 | 
				
			||||||
 | 
					    "allowJs": false                          /* Allow javascript files to be compiled. */,
 | 
				
			||||||
 | 
					    // "checkJs": true,                       /* Report errors in .js files. */
 | 
				
			||||||
 | 
					    "jsx": "react-jsx"                        /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
 | 
				
			||||||
 | 
					    "declaration": true                       /* Generates corresponding '.d.ts' file. */,
 | 
				
			||||||
 | 
					    "declarationMap": true                    /* Generates a sourcemap for each corresponding '.d.ts' file. */,
 | 
				
			||||||
 | 
					    "sourceMap": true                         /* Generates corresponding '.map' file. */,
 | 
				
			||||||
 | 
					    // "outFile": "./",                       /* Concatenate and emit output to single file. */
 | 
				
			||||||
 | 
					    // "outDir": "./lib",                     /* Redirect output structure to the directory. */
 | 
				
			||||||
 | 
					    "rootDir": "./",                          /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
 | 
				
			||||||
 | 
					    // "composite": true,                     /* Enable project compilation */
 | 
				
			||||||
 | 
					    // "removeComments": true,                /* Do not emit comments to output. */
 | 
				
			||||||
 | 
					    "noEmit": true,                           /* Do not emit outputs. */
 | 
				
			||||||
 | 
					    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
 | 
				
			||||||
 | 
					    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
 | 
				
			||||||
 | 
					    "isolatedModules": true,                  /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Strict Type-Checking Options */
 | 
				
			||||||
 | 
					    "strict": true                            /* Enable all strict type-checking options. */,
 | 
				
			||||||
 | 
					    "noImplicitAny": false                    /* Raise error on expressions and declarations with an implied 'any' type. */,
 | 
				
			||||||
 | 
					    // "strictNullChecks": true,              /* Enable strict null checks. */
 | 
				
			||||||
 | 
					    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
 | 
				
			||||||
 | 
					    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
 | 
				
			||||||
 | 
					    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
 | 
				
			||||||
 | 
					    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Additional Checks */
 | 
				
			||||||
 | 
					    // "noUnusedLocals": true,                /* Report errors on unused locals. */
 | 
				
			||||||
 | 
					    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
 | 
				
			||||||
 | 
					    // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
 | 
				
			||||||
 | 
					    "noFallthroughCasesInSwitch": true        /* Report errors for fallthrough cases in switch statement. */,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Module Resolution Options */
 | 
				
			||||||
 | 
					    "moduleResolution": "Bundler"             /* Specify module resolution strategy */,
 | 
				
			||||||
 | 
					    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
 | 
				
			||||||
 | 
					    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
 | 
				
			||||||
 | 
					    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
 | 
				
			||||||
 | 
					    // "typeRoots": [],                       /* List of folders to include type definitions from. */
 | 
				
			||||||
 | 
					    "types": ["vite/client"],                 /* Type declaration files to be included in compilation. */
 | 
				
			||||||
 | 
					    "allowSyntheticDefaultImports": true      /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
 | 
				
			||||||
 | 
					    "esModuleInterop": false                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
 | 
				
			||||||
 | 
					    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
 | 
				
			||||||
 | 
					    "resolveJsonModule": true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Source Map Options */
 | 
				
			||||||
 | 
					    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
 | 
				
			||||||
 | 
					    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
 | 
				
			||||||
 | 
					    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
 | 
				
			||||||
 | 
					    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Experimental Options */
 | 
				
			||||||
 | 
					    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
 | 
				
			||||||
 | 
					    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
 | 
				
			||||||
 | 
					    "skipLibCheck": false
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": ["src"],
 | 
				
			||||||
 | 
					  "references": [{ "path": "./tsconfig.vite.json" }]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								tsconfig.vite.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								tsconfig.vite.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "composite": true,
 | 
				
			||||||
 | 
					    "module": "esnext",
 | 
				
			||||||
 | 
					    "moduleResolution": "node"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": ["vite.config.ts"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,8 @@
 | 
				
			||||||
import { defineConfig } from "vite";
 | 
					 | 
				
			||||||
import react from "@vitejs/plugin-react";
 | 
					 | 
				
			||||||
import { vitePluginVersionMark } from "vite-plugin-version-mark";
 | 
					import { vitePluginVersionMark } from "vite-plugin-version-mark";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import react from "@vitejs/plugin-react";
 | 
				
			||||||
 | 
					import { defineConfig } from "vite";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
  plugins: [
 | 
					  plugins: [
 | 
				
			||||||
    react(),
 | 
					    react(),
 | 
				
			||||||
		Loading…
	
		Reference in a new issue