mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-12 02:00:28 +03:00
Merge branch 'dev' into room-avatars
This commit is contained in:
commit
7686bcd96d
55 changed files with 1991 additions and 866 deletions
2
.github/workflows/build-pull-request.yml
vendored
2
.github/workflows/build-pull-request.yml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version: 20.12.2
|
||||
cache: 'npm'
|
||||
|
|
|
|||
2
.github/workflows/docker-pr.yml
vendored
2
.github/workflows/docker-pr.yml
vendored
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
|
|
|
|||
2
.github/workflows/netlify-dev.yml
vendored
2
.github/workflows/netlify-dev.yml
vendored
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version: 20.12.2
|
||||
cache: 'npm'
|
||||
|
|
|
|||
4
.github/workflows/prod-deploy.yml
vendored
4
.github/workflows/prod-deploy.yml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version: 20.12.2
|
||||
cache: 'npm'
|
||||
|
|
@ -90,7 +90,7 @@ jobs:
|
|||
${{ secrets.DOCKER_USERNAME }}/cinny
|
||||
ghcr.io/${{ github.repository }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
|
|
|||
106
package-lock.json
generated
106
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "cinny",
|
||||
"version": "4.6.0",
|
||||
"version": "4.8.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cinny",
|
||||
"version": "4.6.0",
|
||||
"version": "4.8.1",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
"@vanilla-extract/recipes": "0.3.0",
|
||||
"@vanilla-extract/vite-plugin": "3.7.1",
|
||||
"await-to-js": "3.0.0",
|
||||
"badwords-list": "2.0.1-4",
|
||||
"blurhash": "2.0.4",
|
||||
"browser-encrypt-attachment": "0.3.0",
|
||||
"chroma-js": "3.1.2",
|
||||
|
|
@ -33,7 +34,7 @@
|
|||
"file-saver": "2.0.5",
|
||||
"flux": "4.0.3",
|
||||
"focus-trap-react": "10.0.2",
|
||||
"folds": "2.1.0",
|
||||
"folds": "2.2.0",
|
||||
"formik": "2.4.6",
|
||||
"html-dom-parser": "4.0.0",
|
||||
"html-react-parser": "4.2.0",
|
||||
|
|
@ -45,7 +46,7 @@
|
|||
"jotai": "2.6.0",
|
||||
"linkify-react": "4.1.3",
|
||||
"linkifyjs": "4.1.3",
|
||||
"matrix-js-sdk": "35.0.0",
|
||||
"matrix-js-sdk": "37.5.0",
|
||||
"millify": "6.1.0",
|
||||
"pdfjs-dist": "4.2.67",
|
||||
"prismjs": "1.30.0",
|
||||
|
|
@ -2263,17 +2264,19 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-11.1.0.tgz",
|
||||
"integrity": "sha512-JPuO9RCVDklDjbFzMvZfQb7PuiFkLY72bniRSu81lRzkkrcbZtmKqBFMm9H4f2FSz+tHVkDnmsvn12I4sdJJ5A==",
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-14.1.0.tgz",
|
||||
"integrity": "sha512-vcSxHJIr6lP0Fgo8jl0sTHg+OZxZn+skGjiyB62erfgw/R2QqJl0ZVSY8SRcbk9LtHo/ZGld1tnaOyjL2e3cLQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@matrix-org/olm": {
|
||||
"version": "3.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@matrix-org/olm/-/olm-3.2.15.tgz",
|
||||
"integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q=="
|
||||
"integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
|
|
@ -4589,7 +4592,8 @@
|
|||
"node_modules/@types/events": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz",
|
||||
"integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g=="
|
||||
"integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/file-saver": {
|
||||
"version": "2.0.5",
|
||||
|
|
@ -4678,7 +4682,8 @@
|
|||
"node_modules/@types/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/sanitize-html": {
|
||||
"version": "2.9.0",
|
||||
|
|
@ -5088,7 +5093,8 @@
|
|||
"node_modules/another-json": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz",
|
||||
"integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg=="
|
||||
"integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
|
|
@ -5431,6 +5437,12 @@
|
|||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/badwords-list": {
|
||||
"version": "2.0.1-4",
|
||||
"resolved": "https://registry.npmjs.org/badwords-list/-/badwords-list-2.0.1-4.tgz",
|
||||
"integrity": "sha512-FxfZUp7B9yCnesNtFQS9v6PvZdxTYa14Q60JR6vhjdQdWI4naTjJIyx22JzoER8ooeT8SAAKoHLjKfCV7XgYUQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
|
|
@ -5438,9 +5450,10 @@
|
|||
"devOptional": true
|
||||
},
|
||||
"node_modules/base-x": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz",
|
||||
"integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ=="
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
|
||||
"integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
|
|
@ -5546,6 +5559,7 @@
|
|||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
|
||||
"integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base-x": "^5.0.0"
|
||||
}
|
||||
|
|
@ -5848,6 +5862,7 @@
|
|||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
|
|
@ -6999,6 +7014,7 @@
|
|||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
|
|
@ -7249,15 +7265,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/folds": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/folds/-/folds-2.1.0.tgz",
|
||||
"integrity": "sha512-KwAG8bH3jsyZ9FKPMg+6ABV2YOcpp4nL0cCelsalnaPeRThkc5fgG1Xj5mhmdffYKjEXpEbERi5qmGbepgJryg==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/folds/-/folds-2.2.0.tgz",
|
||||
"integrity": "sha512-uOfck5eWEIK11rhOAEdSoPIvMXwv+D1Go03pxSAKezWVb+uRoBdmE6LEqiLOF+ac4DGmZRMPvpdDsXCg7EVNIg==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@vanilla-extract/css": "^1.9.2",
|
||||
"@vanilla-extract/recipes": "^0.3.0",
|
||||
"classnames": "^2.3.2",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0"
|
||||
"@vanilla-extract/css": "1.9.2",
|
||||
"@vanilla-extract/recipes": "0.3.0",
|
||||
"classnames": "2.3.2",
|
||||
"react": "17.0.0",
|
||||
"react-dom": "17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
|
|
@ -8557,6 +8574,7 @@
|
|||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8689,6 +8707,7 @@
|
|||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
|
||||
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
},
|
||||
|
|
@ -8764,21 +8783,23 @@
|
|||
"node_modules/matrix-events-sdk": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz",
|
||||
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
|
||||
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/matrix-js-sdk": {
|
||||
"version": "35.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-35.0.0.tgz",
|
||||
"integrity": "sha512-X8hIsd/8x1SC9vRr8DiNKQxmdrfRujtvEWPz8mY4FxVDJG8HEGDHvqUmaSy2jrtnOUn4oHzGQVLFO3DnhsSf8w==",
|
||||
"version": "37.5.0",
|
||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-37.5.0.tgz",
|
||||
"integrity": "sha512-5tyuAi5hnKud1UkVq8Z2/3c22hWGELBZzErJPZkE6Hju2uGUfGtrIx6uj6puv0ZjvsUU3X6Qgm8vdReKO1PGig==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^11.0.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^14.0.1",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.7.1",
|
||||
"loglevel": "^1.9.2",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
|
|
@ -8792,21 +8813,23 @@
|
|||
}
|
||||
},
|
||||
"node_modules/matrix-js-sdk/node_modules/uuid": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
|
||||
"integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/matrix-widget-api": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.12.0.tgz",
|
||||
"integrity": "sha512-6JRd9fJGGvuBRhcTg9wX+Skn/Q1wox3jdp5yYQKJ6pPw4urW9bkTR90APBKVDB1vorJKT44jml+lCzkDMRBjww==",
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.13.1.tgz",
|
||||
"integrity": "sha512-mkOHUVzaN018TCbObfGOSaMW2GoUxOfcxNNlTVx5/HeMk3OSQPQM0C9oEME5Liiv/dBUoSrEB64V8wF7e/gb1w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/events": "^3.0.0",
|
||||
"events": "^3.2.0"
|
||||
|
|
@ -9198,9 +9221,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/oidc-client-ts": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.1.0.tgz",
|
||||
"integrity": "sha512-IDopEXjiwjkmJLYZo6BTlvwOtnlSniWZkKZoXforC/oLZHC9wkIxd25Kwtmo5yKFMMVcsp3JY6bhcNJqdYk8+g==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.2.1.tgz",
|
||||
"integrity": "sha512-hS5AJ5s/x4bXhHvNJT1v+GGvzHUwdRWqNQQbSrp10L1IRmzfRGKQ3VWN3dstJb+oF3WtAyKezwD2+dTEIyBiAA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jwt-decode": "^4.0.0"
|
||||
},
|
||||
|
|
@ -9288,6 +9312,7 @@
|
|||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/retry": "0.12.0",
|
||||
"retry": "^0.13.1"
|
||||
|
|
@ -10051,6 +10076,7 @@
|
|||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
|
|
@ -10264,6 +10290,7 @@
|
|||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz",
|
||||
"integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"sdp-verify": "checker.js"
|
||||
}
|
||||
|
|
@ -11172,7 +11199,8 @@
|
|||
"node_modules/unhomoglyph": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz",
|
||||
"integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg=="
|
||||
"integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cinny",
|
||||
"version": "4.6.0",
|
||||
"version": "4.8.1",
|
||||
"description": "Yet another matrix client",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
|
@ -32,6 +32,7 @@
|
|||
"@vanilla-extract/recipes": "0.3.0",
|
||||
"@vanilla-extract/vite-plugin": "3.7.1",
|
||||
"await-to-js": "3.0.0",
|
||||
"badwords-list": "2.0.1-4",
|
||||
"blurhash": "2.0.4",
|
||||
"browser-encrypt-attachment": "0.3.0",
|
||||
"chroma-js": "3.1.2",
|
||||
|
|
@ -44,7 +45,7 @@
|
|||
"file-saver": "2.0.5",
|
||||
"flux": "4.0.3",
|
||||
"focus-trap-react": "10.0.2",
|
||||
"folds": "2.1.0",
|
||||
"folds": "2.2.0",
|
||||
"formik": "2.4.6",
|
||||
"html-dom-parser": "4.0.0",
|
||||
"html-react-parser": "4.2.0",
|
||||
|
|
@ -56,7 +57,7 @@
|
|||
"jotai": "2.6.0",
|
||||
"linkify-react": "4.1.3",
|
||||
"linkifyjs": "4.1.3",
|
||||
"matrix-js-sdk": "35.0.0",
|
||||
"matrix-js-sdk": "37.5.0",
|
||||
"millify": "6.1.0",
|
||||
"pdfjs-dist": "4.2.67",
|
||||
"prismjs": "1.30.0",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,16 @@ import { JoinRule } from 'matrix-js-sdk';
|
|||
import FocusTrap from 'focus-trap-react';
|
||||
import { stopPropagation } from '../utils/keyboard';
|
||||
|
||||
type JoinRuleIcons = Record<JoinRule, IconSrc>;
|
||||
export type ExtraJoinRules = 'knock_restricted';
|
||||
export type ExtendedJoinRules = JoinRule | ExtraJoinRules;
|
||||
|
||||
type JoinRuleIcons = Record<ExtendedJoinRules, IconSrc>;
|
||||
export const useRoomJoinRuleIcon = (): JoinRuleIcons =>
|
||||
useMemo(
|
||||
() => ({
|
||||
[JoinRule.Invite]: Icons.HashLock,
|
||||
[JoinRule.Knock]: Icons.HashLock,
|
||||
knock_restricted: Icons.Hash,
|
||||
[JoinRule.Restricted]: Icons.Hash,
|
||||
[JoinRule.Public]: Icons.HashGlobe,
|
||||
[JoinRule.Private]: Icons.HashLock,
|
||||
|
|
@ -34,6 +38,7 @@ export const useSpaceJoinRuleIcon = (): JoinRuleIcons =>
|
|||
() => ({
|
||||
[JoinRule.Invite]: Icons.SpaceLock,
|
||||
[JoinRule.Knock]: Icons.SpaceLock,
|
||||
knock_restricted: Icons.Space,
|
||||
[JoinRule.Restricted]: Icons.Space,
|
||||
[JoinRule.Public]: Icons.SpaceGlobe,
|
||||
[JoinRule.Private]: Icons.SpaceLock,
|
||||
|
|
@ -41,12 +46,13 @@ export const useSpaceJoinRuleIcon = (): JoinRuleIcons =>
|
|||
[]
|
||||
);
|
||||
|
||||
type JoinRuleLabels = Record<JoinRule, string>;
|
||||
type JoinRuleLabels = Record<ExtendedJoinRules, string>;
|
||||
export const useRoomJoinRuleLabel = (): JoinRuleLabels =>
|
||||
useMemo(
|
||||
() => ({
|
||||
[JoinRule.Invite]: 'Invite Only',
|
||||
[JoinRule.Knock]: 'Knock & Invite',
|
||||
knock_restricted: 'Space Members or Knock',
|
||||
[JoinRule.Restricted]: 'Space Members',
|
||||
[JoinRule.Public]: 'Public',
|
||||
[JoinRule.Private]: 'Invite Only',
|
||||
|
|
@ -54,7 +60,7 @@ export const useRoomJoinRuleLabel = (): JoinRuleLabels =>
|
|||
[]
|
||||
);
|
||||
|
||||
type JoinRulesSwitcherProps<T extends JoinRule[]> = {
|
||||
type JoinRulesSwitcherProps<T extends ExtendedJoinRules[]> = {
|
||||
icons: JoinRuleIcons;
|
||||
labels: JoinRuleLabels;
|
||||
rules: T;
|
||||
|
|
@ -63,7 +69,7 @@ type JoinRulesSwitcherProps<T extends JoinRule[]> = {
|
|||
disabled?: boolean;
|
||||
changing?: boolean;
|
||||
};
|
||||
export function JoinRulesSwitcher<T extends JoinRule[]>({
|
||||
export function JoinRulesSwitcher<T extends ExtendedJoinRules[]>({
|
||||
icons,
|
||||
labels,
|
||||
rules,
|
||||
|
|
@ -79,7 +85,7 @@ export function JoinRulesSwitcher<T extends JoinRule[]>({
|
|||
};
|
||||
|
||||
const handleChange = useCallback(
|
||||
(selectedRule: JoinRule) => {
|
||||
(selectedRule: ExtendedJoinRules) => {
|
||||
setCords(undefined);
|
||||
onChange(selectedRule);
|
||||
},
|
||||
|
|
@ -131,7 +137,7 @@ export function JoinRulesSwitcher<T extends JoinRule[]>({
|
|||
fill="Soft"
|
||||
radii="300"
|
||||
outlined
|
||||
before={<Icon size="100" src={icons[value]} />}
|
||||
before={<Icon size="100" src={icons[value] ?? icons[JoinRule.Restricted]} />}
|
||||
after={
|
||||
changing ? (
|
||||
<Spinner size="100" variant="Secondary" fill="Soft" />
|
||||
|
|
@ -142,7 +148,7 @@ export function JoinRulesSwitcher<T extends JoinRule[]>({
|
|||
onClick={handleOpenMenu}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text size="B300">{labels[value]}</Text>
|
||||
<Text size="B300">{labels[value] ?? 'Unsupported'}</Text>
|
||||
</Button>
|
||||
</PopOut>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { FormEventHandler, useCallback } from 'react';
|
||||
import { Box, Text, Button, Spinner, color } from 'folds';
|
||||
import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase';
|
||||
import { decodeRecoveryKey, deriveRecoveryKeyFromPassphrase } from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { PasswordInput } from './password-input';
|
||||
import {
|
||||
SecretStorageKeyContent,
|
||||
|
|
@ -29,11 +28,16 @@ export function SecretStorageRecoveryPassphrase({
|
|||
const [driveKeyState, submitPassphrase] = useAsyncCallback<
|
||||
Uint8Array,
|
||||
Error,
|
||||
Parameters<typeof deriveKey>
|
||||
Parameters<typeof deriveRecoveryKeyFromPassphrase>
|
||||
>(
|
||||
useCallback(
|
||||
async (passphrase, salt, iterations, bits) => {
|
||||
const decodedRecoveryKey = await deriveKey(passphrase, salt, iterations, bits);
|
||||
const decodedRecoveryKey = await deriveRecoveryKeyFromPassphrase(
|
||||
passphrase,
|
||||
salt,
|
||||
iterations,
|
||||
bits
|
||||
);
|
||||
|
||||
const match = await mx.secretStorage.checkKey(decodedRecoveryKey, keyContent as any);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { stopPropagation } from '../../../utils/keyboard';
|
|||
import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../../utils/matrix';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { ModalWide } from '../../../styles/Modal.css';
|
||||
import { validBlurHash } from '../../../utils/blurHash';
|
||||
|
||||
type RenderViewerProps = {
|
||||
src: string;
|
||||
|
|
@ -77,7 +78,7 @@ export const ImageContent = as<'div', ImageContentProps>(
|
|||
) => {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const blurHash = info?.[MATRIX_BLUR_HASH_PROPERTY_NAME];
|
||||
const blurHash = validBlurHash(info?.[MATRIX_BLUR_HASH_PROPERTY_NAME]);
|
||||
|
||||
const [load, setLoad] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
mxcUrlToHttp,
|
||||
} from '../../../utils/matrix';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { validBlurHash } from '../../../utils/blurHash';
|
||||
|
||||
type RenderVideoProps = {
|
||||
title: string;
|
||||
|
|
@ -68,7 +69,7 @@ export const VideoContent = as<'div', VideoContentProps>(
|
|||
) => {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const blurHash = info.thumbnail_info?.[MATRIX_BLUR_HASH_PROPERTY_NAME];
|
||||
const blurHash = validBlurHash(info.thumbnail_info?.[MATRIX_BLUR_HASH_PROPERTY_NAME]);
|
||||
|
||||
const [load, setLoad] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
|
|
|||
|
|
@ -105,6 +105,20 @@ export const PageContent = as<'div'>(({ className, ...props }, ref) => (
|
|||
<div className={classNames(css.PageContent, className)} {...props} ref={ref} />
|
||||
));
|
||||
|
||||
export function PageHeroEmpty({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Box
|
||||
className={classNames(ContainerColor({ variant: 'SurfaceVariant' }), css.PageHeroEmpty)}
|
||||
direction="Column"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
gap="200"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export const PageHeroSection = as<'div', ComponentProps<typeof Box>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<Box
|
||||
|
|
|
|||
|
|
@ -92,6 +92,15 @@ export const PageContent = style([
|
|||
},
|
||||
]);
|
||||
|
||||
export const PageHeroEmpty = style([
|
||||
DefaultReset,
|
||||
{
|
||||
padding: config.space.S400,
|
||||
borderRadius: config.radii.R400,
|
||||
minHeight: toRem(450),
|
||||
},
|
||||
]);
|
||||
|
||||
export const PageHeroSection = style([
|
||||
DefaultReset,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const TextViewerContent = forwardRef<HTMLPreElement, TextViewerContentPro
|
|||
>
|
||||
<ErrorBoundary fallback={<code>{text}</code>}>
|
||||
<Suspense fallback={<code>{text}</code>}>
|
||||
<ReactPrism>{(codeRef) => <code ref={codeRef}>{text}</code>}</ReactPrism>
|
||||
<ReactPrism key={text}>{(codeRef) => <code ref={codeRef}>{text}</code>}</ReactPrism>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import React, { useCallback, useMemo } from 'react';
|
|||
import { color, Text } from 'folds';
|
||||
import { JoinRule, MatrixError, RestrictedAllowType } from 'matrix-js-sdk';
|
||||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
|
||||
import {
|
||||
ExtendedJoinRules,
|
||||
JoinRulesSwitcher,
|
||||
useRoomJoinRuleIcon,
|
||||
useRoomJoinRuleLabel,
|
||||
|
|
@ -19,6 +21,12 @@ import { useStateEvent } from '../../../hooks/useStateEvent';
|
|||
import { useSpaceOptionally } from '../../../hooks/useSpace';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { getStateEvents } from '../../../utils/room';
|
||||
import {
|
||||
useRecursiveChildSpaceScopeFactory,
|
||||
useSpaceChildren,
|
||||
} from '../../../state/hooks/roomList';
|
||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
|
||||
type RestrictedRoomAllowContent = {
|
||||
room_id: string;
|
||||
|
|
@ -32,9 +40,14 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
|||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const roomVersion = parseInt(room.getVersion(), 10);
|
||||
const allowKnockRestricted = roomVersion >= 10;
|
||||
const allowRestricted = roomVersion >= 8;
|
||||
const allowKnock = roomVersion >= 7;
|
||||
|
||||
const roomIdToParents = useAtomValue(roomToParentsAtom);
|
||||
const space = useSpaceOptionally();
|
||||
const subspacesScope = useRecursiveChildSpaceScopeFactory(mx, roomIdToParents);
|
||||
const subspaces = useSpaceChildren(allRoomsAtom, space?.roomId ?? '', subspacesScope);
|
||||
|
||||
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
|
||||
const canEdit = powerLevelAPI.canSendStateEvent(
|
||||
|
|
@ -47,18 +60,21 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
|||
const content = joinRuleEvent?.getContent<RoomJoinRulesEventContent>();
|
||||
const rule: JoinRule = content?.join_rule ?? JoinRule.Invite;
|
||||
|
||||
const joinRules: Array<JoinRule> = useMemo(() => {
|
||||
const r: JoinRule[] = [JoinRule.Invite];
|
||||
const joinRules: Array<ExtendedJoinRules> = useMemo(() => {
|
||||
const r: ExtendedJoinRules[] = [JoinRule.Invite];
|
||||
if (allowKnock) {
|
||||
r.push(JoinRule.Knock);
|
||||
}
|
||||
if (allowRestricted && space) {
|
||||
r.push(JoinRule.Restricted);
|
||||
}
|
||||
if (allowKnockRestricted && space) {
|
||||
r.push('knock_restricted');
|
||||
}
|
||||
r.push(JoinRule.Public);
|
||||
|
||||
return r;
|
||||
}, [allowRestricted, allowKnock, space]);
|
||||
}, [allowKnockRestricted, allowRestricted, allowKnock, space]);
|
||||
|
||||
const icons = useRoomJoinRuleIcon();
|
||||
const spaceIcons = useSpaceJoinRuleIcon();
|
||||
|
|
@ -66,12 +82,25 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
|||
|
||||
const [submitState, submit] = useAsyncCallback(
|
||||
useCallback(
|
||||
async (joinRule: JoinRule) => {
|
||||
async (joinRule: ExtendedJoinRules) => {
|
||||
const allow: RestrictedRoomAllowContent[] = [];
|
||||
if (joinRule === JoinRule.Restricted) {
|
||||
const parents = getStateEvents(room, StateEvent.SpaceParent).map((event) =>
|
||||
event.getStateKey()
|
||||
);
|
||||
if (joinRule === JoinRule.Restricted || joinRule === 'knock_restricted') {
|
||||
const roomParents = roomIdToParents.get(room.roomId);
|
||||
|
||||
const parents = getStateEvents(room, StateEvent.SpaceParent)
|
||||
.map((event) => event.getStateKey())
|
||||
.filter((parentId) => typeof parentId === 'string')
|
||||
.filter((parentId) => roomParents?.has(parentId));
|
||||
|
||||
if (parents.length === 0 && space && roomParents) {
|
||||
// if no m.space.parent found
|
||||
// find parent in current space
|
||||
const selectedParents = subspaces.filter((rId) => roomParents.has(rId));
|
||||
if (roomParents.has(space.roomId)) {
|
||||
selectedParents.push(space.roomId);
|
||||
}
|
||||
selectedParents.forEach((pId) => parents.push(pId));
|
||||
}
|
||||
parents.forEach((parentRoomId) => {
|
||||
if (!parentRoomId) return;
|
||||
allow.push({
|
||||
|
|
@ -82,12 +111,12 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
|||
}
|
||||
|
||||
const c: RoomJoinRulesEventContent = {
|
||||
join_rule: joinRule,
|
||||
join_rule: joinRule as JoinRule,
|
||||
};
|
||||
if (allow.length > 0) c.allow = allow;
|
||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomJoinRules as any, c);
|
||||
},
|
||||
[mx, room]
|
||||
[mx, room, space, subspaces, roomIdToParents]
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { MouseEventHandler, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { Box, Icon, IconButton, Icons, Line, Scroll, config } from 'folds';
|
||||
import { Box, Chip, Icon, IconButton, Icons, Line, Scroll, Spinner, Text, config } from 'folds';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
|
@ -36,7 +36,7 @@ import { makeLobbyCategoryId } from '../../state/closedLobbyCategories';
|
|||
import { useCategoryHandler } from '../../hooks/useCategoryHandler';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { allRoomsAtom } from '../../state/room-list/roomList';
|
||||
import { getCanonicalAliasOrRoomId } from '../../utils/matrix';
|
||||
import { getCanonicalAliasOrRoomId, rateLimitedActions } from '../../utils/matrix';
|
||||
import { getSpaceRoomPath } from '../../pages/pathUtils';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
import { CanDropCallback, useDnDMonitor } from './DnD';
|
||||
|
|
@ -53,6 +53,95 @@ import { roomToParentsAtom } from '../../state/room/roomToParents';
|
|||
import { AccountDataEvent } from '../../../types/matrix/accountData';
|
||||
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
||||
import { SpaceHierarchy } from './SpaceHierarchy';
|
||||
import { useGetRoom } from '../../hooks/useGetRoom';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
|
||||
const useCanDropLobbyItem = (
|
||||
space: Room,
|
||||
roomsPowerLevels: Map<string, IPowerLevels>,
|
||||
getRoom: (roomId: string) => Room | undefined,
|
||||
canEditSpaceChild: (powerLevels: IPowerLevels) => boolean
|
||||
): CanDropCallback => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const canDropSpace: CanDropCallback = useCallback(
|
||||
(item, container) => {
|
||||
if (!('space' in container.item)) {
|
||||
// can not drop around rooms.
|
||||
// space can only be drop around other spaces
|
||||
return false;
|
||||
}
|
||||
|
||||
const containerSpaceId = space.roomId;
|
||||
|
||||
if (
|
||||
getRoom(containerSpaceId) === undefined ||
|
||||
!canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[space, roomsPowerLevels, getRoom, canEditSpaceChild]
|
||||
);
|
||||
|
||||
const canDropRoom: CanDropCallback = useCallback(
|
||||
(item, container) => {
|
||||
const containerSpaceId =
|
||||
'space' in container.item ? container.item.roomId : container.item.parentId;
|
||||
|
||||
const draggingOutsideSpace = item.parentId !== containerSpaceId;
|
||||
const restrictedItem = mx.getRoom(item.roomId)?.getJoinRule() === JoinRule.Restricted;
|
||||
|
||||
// check and do not allow restricted room to be dragged outside
|
||||
// current space if can't change `m.room.join_rules` `content.allow`
|
||||
if (draggingOutsideSpace && restrictedItem) {
|
||||
const itemPowerLevel = roomsPowerLevels.get(item.roomId) ?? {};
|
||||
const userPLInItem = powerLevelAPI.getPowerLevel(
|
||||
itemPowerLevel,
|
||||
mx.getUserId() ?? undefined
|
||||
);
|
||||
const canChangeJoinRuleAllow = powerLevelAPI.canSendStateEvent(
|
||||
itemPowerLevel,
|
||||
StateEvent.RoomJoinRules,
|
||||
userPLInItem
|
||||
);
|
||||
if (!canChangeJoinRuleAllow) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
getRoom(containerSpaceId) === undefined ||
|
||||
!canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[mx, getRoom, canEditSpaceChild, roomsPowerLevels]
|
||||
);
|
||||
|
||||
const canDrop: CanDropCallback = useCallback(
|
||||
(item, container): boolean => {
|
||||
if (item.roomId === container.item.roomId || item.roomId === container.nextRoomId) {
|
||||
// can not drop before or after itself
|
||||
return false;
|
||||
}
|
||||
|
||||
// if we are dragging a space
|
||||
if ('space' in item) {
|
||||
return canDropSpace(item, container);
|
||||
}
|
||||
|
||||
return canDropRoom(item, container);
|
||||
},
|
||||
[canDropSpace, canDropRoom]
|
||||
);
|
||||
|
||||
return canDrop;
|
||||
};
|
||||
|
||||
export function Lobby() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -92,15 +181,7 @@ export function Lobby() {
|
|||
useCallback((w, height) => setHeroSectionHeight(height), [])
|
||||
);
|
||||
|
||||
const getRoom = useCallback(
|
||||
(rId: string) => {
|
||||
if (allJoinedRooms.has(rId)) {
|
||||
return mx.getRoom(rId) ?? undefined;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
[mx, allJoinedRooms]
|
||||
);
|
||||
const getRoom = useGetRoom(allJoinedRooms);
|
||||
|
||||
const canEditSpaceChild = useCallback(
|
||||
(powerLevels: IPowerLevels) =>
|
||||
|
|
@ -150,180 +231,155 @@ export function Lobby() {
|
|||
)
|
||||
);
|
||||
|
||||
const canDrop: CanDropCallback = useCallback(
|
||||
(item, container): boolean => {
|
||||
const restrictedItem = mx.getRoom(item.roomId)?.getJoinRule() === JoinRule.Restricted;
|
||||
if (item.roomId === container.item.roomId || item.roomId === container.nextRoomId) {
|
||||
// can not drop before or after itself
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('space' in item) {
|
||||
if (!('space' in container.item)) return false;
|
||||
const containerSpaceId = space.roomId;
|
||||
|
||||
if (
|
||||
getRoom(containerSpaceId) === undefined ||
|
||||
!canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const containerSpaceId =
|
||||
'space' in container.item ? container.item.roomId : container.item.parentId;
|
||||
|
||||
const dropOutsideSpace = item.parentId !== containerSpaceId;
|
||||
|
||||
if (dropOutsideSpace && restrictedItem) {
|
||||
// do not allow restricted room to drop outside
|
||||
// current space if can't change join rule allow
|
||||
const itemPowerLevel = roomsPowerLevels.get(item.roomId) ?? {};
|
||||
const userPLInItem = powerLevelAPI.getPowerLevel(
|
||||
itemPowerLevel,
|
||||
mx.getUserId() ?? undefined
|
||||
);
|
||||
const canChangeJoinRuleAllow = powerLevelAPI.canSendStateEvent(
|
||||
itemPowerLevel,
|
||||
StateEvent.RoomJoinRules,
|
||||
userPLInItem
|
||||
);
|
||||
if (!canChangeJoinRuleAllow) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
getRoom(containerSpaceId) === undefined ||
|
||||
!canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[getRoom, space.roomId, roomsPowerLevels, canEditSpaceChild, mx]
|
||||
const canDrop: CanDropCallback = useCanDropLobbyItem(
|
||||
space,
|
||||
roomsPowerLevels,
|
||||
getRoom,
|
||||
canEditSpaceChild
|
||||
);
|
||||
|
||||
const reorderSpace = useCallback(
|
||||
(item: HierarchyItemSpace, containerItem: HierarchyItem) => {
|
||||
if (!item.parentId) return;
|
||||
const [reorderSpaceState, reorderSpace] = useAsyncCallback(
|
||||
useCallback(
|
||||
async (item: HierarchyItemSpace, containerItem: HierarchyItem) => {
|
||||
if (!item.parentId) return;
|
||||
|
||||
const itemSpaces: HierarchyItemSpace[] = hierarchy
|
||||
.map((i) => i.space)
|
||||
.filter((i) => i.roomId !== item.roomId);
|
||||
const itemSpaces: HierarchyItemSpace[] = hierarchy
|
||||
.map((i) => i.space)
|
||||
.filter((i) => i.roomId !== item.roomId);
|
||||
|
||||
const beforeIndex = itemSpaces.findIndex((i) => i.roomId === containerItem.roomId);
|
||||
const insertIndex = beforeIndex + 1;
|
||||
const beforeIndex = itemSpaces.findIndex((i) => i.roomId === containerItem.roomId);
|
||||
const insertIndex = beforeIndex + 1;
|
||||
|
||||
itemSpaces.splice(insertIndex, 0, {
|
||||
...item,
|
||||
content: { ...item.content, order: undefined },
|
||||
});
|
||||
itemSpaces.splice(insertIndex, 0, {
|
||||
...item,
|
||||
content: { ...item.content, order: undefined },
|
||||
});
|
||||
|
||||
const currentOrders = itemSpaces.map((i) => {
|
||||
if (typeof i.content.order === 'string' && lex.has(i.content.order)) {
|
||||
return i.content.order;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const currentOrders = itemSpaces.map((i) => {
|
||||
if (typeof i.content.order === 'string' && lex.has(i.content.order)) {
|
||||
return i.content.order;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const newOrders = orderKeys(lex, currentOrders);
|
||||
const newOrders = orderKeys(lex, currentOrders);
|
||||
|
||||
newOrders?.forEach((orderKey, index) => {
|
||||
const itm = itemSpaces[index];
|
||||
if (!itm || !itm.parentId) return;
|
||||
const parentPL = roomsPowerLevels.get(itm.parentId);
|
||||
const canEdit = parentPL && canEditSpaceChild(parentPL);
|
||||
if (canEdit && orderKey !== currentOrders[index]) {
|
||||
mx.sendStateEvent(
|
||||
itm.parentId,
|
||||
StateEvent.SpaceChild as any,
|
||||
{ ...itm.content, order: orderKey },
|
||||
itm.roomId
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
[mx, hierarchy, lex, roomsPowerLevels, canEditSpaceChild]
|
||||
);
|
||||
const reorders = newOrders
|
||||
?.map((orderKey, index) => ({
|
||||
item: itemSpaces[index],
|
||||
orderKey,
|
||||
}))
|
||||
.filter((reorder, index) => {
|
||||
if (!reorder.item.parentId) return false;
|
||||
const parentPL = roomsPowerLevels.get(reorder.item.parentId);
|
||||
const canEdit = parentPL && canEditSpaceChild(parentPL);
|
||||
return canEdit && reorder.orderKey !== currentOrders[index];
|
||||
});
|
||||
|
||||
const reorderRoom = useCallback(
|
||||
(item: HierarchyItem, containerItem: HierarchyItem): void => {
|
||||
const itemRoom = mx.getRoom(item.roomId);
|
||||
if (!item.parentId) {
|
||||
return;
|
||||
}
|
||||
const containerParentId: string =
|
||||
'space' in containerItem ? containerItem.roomId : containerItem.parentId;
|
||||
const itemContent = item.content;
|
||||
|
||||
if (item.parentId !== containerParentId) {
|
||||
mx.sendStateEvent(item.parentId, StateEvent.SpaceChild as any, {}, item.roomId);
|
||||
}
|
||||
|
||||
if (
|
||||
itemRoom &&
|
||||
itemRoom.getJoinRule() === JoinRule.Restricted &&
|
||||
item.parentId !== containerParentId
|
||||
) {
|
||||
// change join rule allow parameter when dragging
|
||||
// restricted room from one space to another
|
||||
const joinRuleContent = getStateEvent(
|
||||
itemRoom,
|
||||
StateEvent.RoomJoinRules
|
||||
)?.getContent<RoomJoinRulesEventContent>();
|
||||
|
||||
if (joinRuleContent) {
|
||||
const allow =
|
||||
joinRuleContent.allow?.filter((allowRule) => allowRule.room_id !== item.parentId) ?? [];
|
||||
allow.push({ type: RestrictedAllowType.RoomMembership, room_id: containerParentId });
|
||||
mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as any, {
|
||||
...joinRuleContent,
|
||||
allow,
|
||||
if (reorders) {
|
||||
await rateLimitedActions(reorders, async (reorder) => {
|
||||
if (!reorder.item.parentId) return;
|
||||
await mx.sendStateEvent(
|
||||
reorder.item.parentId,
|
||||
StateEvent.SpaceChild as any,
|
||||
{ ...reorder.item.content, order: reorder.orderKey },
|
||||
reorder.item.roomId
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const itemSpaces = Array.from(
|
||||
hierarchy?.find((i) => i.space.roomId === containerParentId)?.rooms ?? []
|
||||
);
|
||||
|
||||
const beforeItem: HierarchyItem | undefined =
|
||||
'space' in containerItem ? undefined : containerItem;
|
||||
const beforeIndex = itemSpaces.findIndex((i) => i.roomId === beforeItem?.roomId);
|
||||
const insertIndex = beforeIndex + 1;
|
||||
|
||||
itemSpaces.splice(insertIndex, 0, {
|
||||
...item,
|
||||
parentId: containerParentId,
|
||||
content: { ...itemContent, order: undefined },
|
||||
});
|
||||
|
||||
const currentOrders = itemSpaces.map((i) => {
|
||||
if (typeof i.content.order === 'string' && lex.has(i.content.order)) {
|
||||
return i.content.order;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const newOrders = orderKeys(lex, currentOrders);
|
||||
|
||||
newOrders?.forEach((orderKey, index) => {
|
||||
const itm = itemSpaces[index];
|
||||
if (itm && orderKey !== currentOrders[index]) {
|
||||
mx.sendStateEvent(
|
||||
containerParentId,
|
||||
StateEvent.SpaceChild as any,
|
||||
{ ...itm.content, order: orderKey },
|
||||
itm.roomId
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
[mx, hierarchy, lex]
|
||||
},
|
||||
[mx, hierarchy, lex, roomsPowerLevels, canEditSpaceChild]
|
||||
)
|
||||
);
|
||||
const reorderingSpace = reorderSpaceState.status === AsyncStatus.Loading;
|
||||
|
||||
const [reorderRoomState, reorderRoom] = useAsyncCallback(
|
||||
useCallback(
|
||||
async (item: HierarchyItem, containerItem: HierarchyItem) => {
|
||||
const itemRoom = mx.getRoom(item.roomId);
|
||||
if (!item.parentId) {
|
||||
return;
|
||||
}
|
||||
const containerParentId: string =
|
||||
'space' in containerItem ? containerItem.roomId : containerItem.parentId;
|
||||
const itemContent = item.content;
|
||||
|
||||
// remove from current space
|
||||
if (item.parentId !== containerParentId) {
|
||||
mx.sendStateEvent(item.parentId, StateEvent.SpaceChild as any, {}, item.roomId);
|
||||
}
|
||||
|
||||
if (
|
||||
itemRoom &&
|
||||
itemRoom.getJoinRule() === JoinRule.Restricted &&
|
||||
item.parentId !== containerParentId
|
||||
) {
|
||||
// change join rule allow parameter when dragging
|
||||
// restricted room from one space to another
|
||||
const joinRuleContent = getStateEvent(
|
||||
itemRoom,
|
||||
StateEvent.RoomJoinRules
|
||||
)?.getContent<RoomJoinRulesEventContent>();
|
||||
|
||||
if (joinRuleContent) {
|
||||
const allow =
|
||||
joinRuleContent.allow?.filter((allowRule) => allowRule.room_id !== item.parentId) ??
|
||||
[];
|
||||
allow.push({ type: RestrictedAllowType.RoomMembership, room_id: containerParentId });
|
||||
mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as any, {
|
||||
...joinRuleContent,
|
||||
allow,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const itemSpaces = Array.from(
|
||||
hierarchy?.find((i) => i.space.roomId === containerParentId)?.rooms ?? []
|
||||
);
|
||||
|
||||
const beforeItem: HierarchyItem | undefined =
|
||||
'space' in containerItem ? undefined : containerItem;
|
||||
const beforeIndex = itemSpaces.findIndex((i) => i.roomId === beforeItem?.roomId);
|
||||
const insertIndex = beforeIndex + 1;
|
||||
|
||||
itemSpaces.splice(insertIndex, 0, {
|
||||
...item,
|
||||
parentId: containerParentId,
|
||||
content: { ...itemContent, order: undefined },
|
||||
});
|
||||
|
||||
const currentOrders = itemSpaces.map((i) => {
|
||||
if (typeof i.content.order === 'string' && lex.has(i.content.order)) {
|
||||
return i.content.order;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const newOrders = orderKeys(lex, currentOrders);
|
||||
|
||||
const reorders = newOrders
|
||||
?.map((orderKey, index) => ({
|
||||
item: itemSpaces[index],
|
||||
orderKey,
|
||||
}))
|
||||
.filter((reorder, index) => reorder.item && reorder.orderKey !== currentOrders[index]);
|
||||
|
||||
if (reorders) {
|
||||
await rateLimitedActions(reorders, async (reorder) => {
|
||||
await mx.sendStateEvent(
|
||||
containerParentId,
|
||||
StateEvent.SpaceChild as any,
|
||||
{ ...reorder.item.content, order: reorder.orderKey },
|
||||
reorder.item.roomId
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
[mx, hierarchy, lex]
|
||||
)
|
||||
);
|
||||
const reorderingRoom = reorderRoomState.status === AsyncStatus.Loading;
|
||||
const reordering = reorderingRoom || reorderingSpace;
|
||||
|
||||
useDnDMonitor(
|
||||
scrollRef,
|
||||
|
|
@ -449,6 +505,7 @@ export function Lobby() {
|
|||
draggingItem={draggingItem}
|
||||
onDragging={setDraggingItem}
|
||||
canDrop={canDrop}
|
||||
disabledReorder={reordering}
|
||||
nextSpaceId={nextSpaceId}
|
||||
getRoom={getRoom}
|
||||
pinned={sidebarSpaces.has(item.space.roomId)}
|
||||
|
|
@ -460,6 +517,28 @@ export function Lobby() {
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
{reordering && (
|
||||
<Box
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: config.space.S400,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 2,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
justifyContent="Center"
|
||||
>
|
||||
<Chip
|
||||
variant="Secondary"
|
||||
outlined
|
||||
radii="Pill"
|
||||
before={<Spinner variant="Secondary" fill="Soft" size="100" />}
|
||||
>
|
||||
<Text size="L400">Reordering</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
)}
|
||||
</PageContentCenter>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ type SpaceHierarchyProps = {
|
|||
draggingItem?: HierarchyItem;
|
||||
onDragging: (item?: HierarchyItem) => void;
|
||||
canDrop: CanDropCallback;
|
||||
disabledReorder?: boolean;
|
||||
nextSpaceId?: string;
|
||||
getRoom: (roomId: string) => Room | undefined;
|
||||
pinned: boolean;
|
||||
|
|
@ -54,6 +55,7 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
|
|||
draggingItem,
|
||||
onDragging,
|
||||
canDrop,
|
||||
disabledReorder,
|
||||
nextSpaceId,
|
||||
getRoom,
|
||||
pinned,
|
||||
|
|
@ -116,7 +118,9 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
|
|||
handleClose={handleClose}
|
||||
getRoom={getRoom}
|
||||
canEditChild={canEditSpaceChild(spacePowerLevels)}
|
||||
canReorder={parentPowerLevels ? canEditSpaceChild(parentPowerLevels) : false}
|
||||
canReorder={
|
||||
parentPowerLevels && !disabledReorder ? canEditSpaceChild(parentPowerLevels) : false
|
||||
}
|
||||
options={
|
||||
parentId &&
|
||||
parentPowerLevels && (
|
||||
|
|
@ -174,7 +178,7 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
|
|||
dm={mDirects.has(roomItem.roomId)}
|
||||
onOpen={onOpenRoom}
|
||||
getRoom={getRoom}
|
||||
canReorder={canEditSpaceChild(spacePowerLevels)}
|
||||
canReorder={canEditSpaceChild(spacePowerLevels) && !disabledReorder}
|
||||
options={
|
||||
<HierarchyItemMenu
|
||||
item={roomItem}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
|||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { SearchOrderBy } from 'matrix-js-sdk';
|
||||
import { PageHero, PageHeroSection } from '../../components/page';
|
||||
import { PageHero, PageHeroEmpty, PageHeroSection } from '../../components/page';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { _SearchPathSearchParams } from '../../pages/paths';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
|
|
@ -222,18 +222,7 @@ export function MessageSearch({
|
|||
</Box>
|
||||
|
||||
{!msgSearchParams.term && status === 'pending' && (
|
||||
<Box
|
||||
className={ContainerColor({ variant: 'SurfaceVariant' })}
|
||||
style={{
|
||||
padding: config.space.S400,
|
||||
borderRadius: config.radii.R400,
|
||||
minHeight: toRem(450),
|
||||
}}
|
||||
direction="Column"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
gap="200"
|
||||
>
|
||||
<PageHeroEmpty>
|
||||
<PageHeroSection>
|
||||
<PageHero
|
||||
icon={<Icon size="600" src={Icons.Message} />}
|
||||
|
|
@ -241,7 +230,7 @@ export function MessageSearch({
|
|||
subTitle="Find helpful messages in your community by searching with related keywords."
|
||||
/>
|
||||
</PageHeroSection>
|
||||
</Box>
|
||||
</PageHeroEmpty>
|
||||
)}
|
||||
|
||||
{msgSearchParams.term && groups.length === 0 && status === 'success' && (
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export function SearchInput({ active, loading, searchInputRef, onSearch, onReset
|
|||
ref={searchInputRef}
|
||||
style={{ paddingRight: config.space.S300 }}
|
||||
name="searchInput"
|
||||
autoFocus
|
||||
size="500"
|
||||
variant="Background"
|
||||
placeholder="Search for keyword"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo } from 'react';
|
||||
import { Editor } from 'slate';
|
||||
import { Box, MenuItem, Text } from 'folds';
|
||||
import { Box, config, MenuItem, Text } from 'folds';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { Command, useCommands } from '../../hooks/useCommands';
|
||||
import {
|
||||
|
|
@ -75,9 +75,6 @@ export function CommandAutocomplete({
|
|||
headerContent={
|
||||
<Box grow="Yes" direction="Row" gap="200" justifyContent="SpaceBetween">
|
||||
<Text size="L400">Commands</Text>
|
||||
<Text size="T200" priority="300" truncate>
|
||||
Begin your message with command
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
requestClose={requestClose}
|
||||
|
|
@ -87,17 +84,22 @@ export function CommandAutocomplete({
|
|||
key={commandName}
|
||||
as="button"
|
||||
radii="300"
|
||||
style={{ height: 'unset' }}
|
||||
onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
|
||||
onTabPress(evt, () => handleAutocomplete(commandName))
|
||||
}
|
||||
onClick={() => handleAutocomplete(commandName)}
|
||||
>
|
||||
<Box grow="Yes" direction="Row" gap="200" justifyContent="SpaceBetween">
|
||||
<Box shrink="No">
|
||||
<Text style={{ flexGrow: 1 }} size="B400" truncate>
|
||||
{`/${commandName}`}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
style={{ padding: `${config.space.S300} 0` }}
|
||||
grow="Yes"
|
||||
direction="Column"
|
||||
gap="100"
|
||||
justifyContent="SpaceBetween"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} size="B400" truncate>
|
||||
{`/${commandName}`}
|
||||
</Text>
|
||||
<Text truncate priority="300" size="T200">
|
||||
{commands[commandName].description}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -448,6 +448,7 @@ export function RoomTimeline({
|
|||
const [encUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
|
||||
const showUrlPreview = room.hasEncryptionStateEvent() ? encUrlPreview : urlPreview;
|
||||
const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
|
||||
const [showDeveloperTools] = useSetting(settingsAtom, 'developerTools');
|
||||
|
||||
const ignoredUsersList = useIgnoredUsers();
|
||||
const ignoredUsersSet = useMemo(() => new Set(ignoredUsersList), [ignoredUsersList]);
|
||||
|
|
@ -1065,6 +1066,7 @@ export function RoomTimeline({
|
|||
)
|
||||
}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
|
|
@ -1146,6 +1148,7 @@ export function RoomTimeline({
|
|||
)
|
||||
}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
|
|
@ -1247,6 +1250,7 @@ export function RoomTimeline({
|
|||
)
|
||||
}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
powerLevelTag={getPowerLevelTag(senderPowerLevel)}
|
||||
accessibleTagColors={accessibleTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
|
|
@ -1292,6 +1296,7 @@ export function RoomTimeline({
|
|||
messageSpacing={messageSpacing}
|
||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
>
|
||||
<EventContent
|
||||
messageLayout={messageLayout}
|
||||
|
|
@ -1328,6 +1333,7 @@ export function RoomTimeline({
|
|||
messageSpacing={messageSpacing}
|
||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
>
|
||||
<EventContent
|
||||
messageLayout={messageLayout}
|
||||
|
|
@ -1365,6 +1371,7 @@ export function RoomTimeline({
|
|||
messageSpacing={messageSpacing}
|
||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
>
|
||||
<EventContent
|
||||
messageLayout={messageLayout}
|
||||
|
|
@ -1402,6 +1409,7 @@ export function RoomTimeline({
|
|||
messageSpacing={messageSpacing}
|
||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
>
|
||||
<EventContent
|
||||
messageLayout={messageLayout}
|
||||
|
|
@ -1441,6 +1449,7 @@ export function RoomTimeline({
|
|||
messageSpacing={messageSpacing}
|
||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
>
|
||||
<EventContent
|
||||
messageLayout={messageLayout}
|
||||
|
|
@ -1485,6 +1494,7 @@ export function RoomTimeline({
|
|||
messageSpacing={messageSpacing}
|
||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
>
|
||||
<EventContent
|
||||
messageLayout={messageLayout}
|
||||
|
|
|
|||
|
|
@ -675,6 +675,7 @@ export type MessageProps = {
|
|||
reply?: ReactNode;
|
||||
reactions?: ReactNode;
|
||||
hideReadReceipts?: boolean;
|
||||
showDeveloperTools?: boolean;
|
||||
powerLevelTag?: PowerLevelTag;
|
||||
accessibleTagColors?: Map<string, string>;
|
||||
legacyUsernameColor?: boolean;
|
||||
|
|
@ -703,6 +704,7 @@ export const Message = as<'div', MessageProps>(
|
|||
reply,
|
||||
reactions,
|
||||
hideReadReceipts,
|
||||
showDeveloperTools,
|
||||
powerLevelTag,
|
||||
accessibleTagColors,
|
||||
legacyUsernameColor,
|
||||
|
|
@ -1026,7 +1028,13 @@ export const Message = as<'div', MessageProps>(
|
|||
onClose={closeMenu}
|
||||
/>
|
||||
)}
|
||||
<MessageSourceCodeItem room={room} mEvent={mEvent} onClose={closeMenu} />
|
||||
{showDeveloperTools && (
|
||||
<MessageSourceCodeItem
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
)}
|
||||
<MessageCopyLinkItem room={room} mEvent={mEvent} onClose={closeMenu} />
|
||||
{canPinEvent && (
|
||||
<MessagePinItem room={room} mEvent={mEvent} onClose={closeMenu} />
|
||||
|
|
@ -1101,6 +1109,7 @@ export type EventProps = {
|
|||
canDelete?: boolean;
|
||||
messageSpacing: MessageSpacing;
|
||||
hideReadReceipts?: boolean;
|
||||
showDeveloperTools?: boolean;
|
||||
};
|
||||
export const Event = as<'div', EventProps>(
|
||||
(
|
||||
|
|
@ -1112,6 +1121,7 @@ export const Event = as<'div', EventProps>(
|
|||
canDelete,
|
||||
messageSpacing,
|
||||
hideReadReceipts,
|
||||
showDeveloperTools,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
|
|
@ -1188,7 +1198,13 @@ export const Event = as<'div', EventProps>(
|
|||
onClose={closeMenu}
|
||||
/>
|
||||
)}
|
||||
<MessageSourceCodeItem room={room} mEvent={mEvent} onClose={closeMenu} />
|
||||
{showDeveloperTools && (
|
||||
<MessageSourceCodeItem
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
)}
|
||||
<MessageCopyLinkItem room={room} mEvent={mEvent} onClose={closeMenu} />
|
||||
</Box>
|
||||
{((!mEvent.isRedacted() && canDelete && !stateEvent) ||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ const useSettingsMenuItems = (): SettingsMenuItem[] =>
|
|||
{
|
||||
page: SettingsPages.DevicesPage,
|
||||
name: 'Devices',
|
||||
icon: Icons.Category,
|
||||
icon: Icons.Monitor,
|
||||
},
|
||||
{
|
||||
page: SettingsPages.EmojisStickersPage,
|
||||
|
|
|
|||
|
|
@ -1,396 +1,10 @@
|
|||
import React, {
|
||||
ChangeEventHandler,
|
||||
FormEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
IconButton,
|
||||
Icon,
|
||||
Icons,
|
||||
Scroll,
|
||||
Input,
|
||||
Avatar,
|
||||
Button,
|
||||
Chip,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
Modal,
|
||||
Dialog,
|
||||
Header,
|
||||
config,
|
||||
Spinner,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import React from 'react';
|
||||
import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
|
||||
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
||||
import { UserAvatar } from '../../../components/user-avatar';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { nameInitials } from '../../../utils/common';
|
||||
import { copyToClipboard } from '../../../utils/dom';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useFilePicker } from '../../../hooks/useFilePicker';
|
||||
import { useObjectURL } from '../../../hooks/useObjectURL';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { ImageEditor } from '../../../components/image-editor';
|
||||
import { ModalWide } from '../../../styles/Modal.css';
|
||||
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
|
||||
import { CompactUploadCardRenderer } from '../../../components/upload-card';
|
||||
import { useCapabilities } from '../../../hooks/useCapabilities';
|
||||
|
||||
function MatrixId() {
|
||||
const mx = useMatrixClient();
|
||||
const userId = mx.getUserId()!;
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Matrix ID</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={userId}
|
||||
after={
|
||||
<Chip variant="Secondary" radii="Pill" onClick={() => copyToClipboard(userId)}>
|
||||
<Text size="T200">Copy</Text>
|
||||
</Chip>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type ProfileProps = {
|
||||
profile: UserProfile;
|
||||
userId: string;
|
||||
};
|
||||
function ProfileAvatar({ profile, userId }: ProfileProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const capabilities = useCapabilities();
|
||||
const [alertRemove, setAlertRemove] = useState(false);
|
||||
const disableSetAvatar = capabilities['m.set_avatar_url']?.enabled === false;
|
||||
|
||||
const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
|
||||
const avatarUrl = profile.avatarUrl
|
||||
? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||
: undefined;
|
||||
|
||||
const [imageFile, setImageFile] = useState<File>();
|
||||
const imageFileURL = useObjectURL(imageFile);
|
||||
const uploadAtom = useMemo(() => {
|
||||
if (imageFile) return createUploadAtom(imageFile);
|
||||
return undefined;
|
||||
}, [imageFile]);
|
||||
|
||||
const pickFile = useFilePicker(setImageFile, false);
|
||||
|
||||
const handleRemoveUpload = useCallback(() => {
|
||||
setImageFile(undefined);
|
||||
}, []);
|
||||
|
||||
const handleUploaded = useCallback(
|
||||
(upload: UploadSuccess) => {
|
||||
const { mxc } = upload;
|
||||
mx.setAvatarUrl(mxc);
|
||||
handleRemoveUpload();
|
||||
},
|
||||
[mx, handleRemoveUpload]
|
||||
);
|
||||
|
||||
const handleRemoveAvatar = () => {
|
||||
mx.setAvatarUrl('');
|
||||
setAlertRemove(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingTile
|
||||
title={
|
||||
<Text as="span" size="L400">
|
||||
Avatar
|
||||
</Text>
|
||||
}
|
||||
after={
|
||||
<Avatar size="500" radii="300">
|
||||
<UserAvatar
|
||||
userId={userId}
|
||||
src={avatarUrl}
|
||||
renderFallback={() => <Text size="H4">{nameInitials(defaultDisplayName)}</Text>}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
{uploadAtom ? (
|
||||
<Box gap="200" direction="Column">
|
||||
<CompactUploadCardRenderer
|
||||
uploadAtom={uploadAtom}
|
||||
onRemove={handleRemoveUpload}
|
||||
onComplete={handleUploaded}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box gap="200">
|
||||
<Button
|
||||
onClick={() => pickFile('image/*')}
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
outlined
|
||||
radii="300"
|
||||
disabled={disableSetAvatar}
|
||||
>
|
||||
<Text size="B300">Upload</Text>
|
||||
</Button>
|
||||
{avatarUrl && (
|
||||
<Button
|
||||
size="300"
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
radii="300"
|
||||
disabled={disableSetAvatar}
|
||||
onClick={() => setAlertRemove(true)}
|
||||
>
|
||||
<Text size="B300">Remove</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{imageFileURL && (
|
||||
<Overlay open={false} backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: handleRemoveUpload,
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Modal className={ModalWide} variant="Surface" size="500">
|
||||
<ImageEditor
|
||||
name={imageFile?.name ?? 'Unnamed'}
|
||||
url={imageFileURL}
|
||||
requestClose={handleRemoveUpload}
|
||||
/>
|
||||
</Modal>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
)}
|
||||
|
||||
<Overlay open={alertRemove} backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setAlertRemove(false),
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Dialog variant="Surface">
|
||||
<Header
|
||||
style={{
|
||||
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
}}
|
||||
variant="Surface"
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="H4">Remove Avatar</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={() => setAlertRemove(false)} radii="300">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||
<Box direction="Column" gap="200">
|
||||
<Text priority="400">Are you sure you want to remove profile avatar?</Text>
|
||||
</Box>
|
||||
<Button variant="Critical" onClick={handleRemoveAvatar}>
|
||||
<Text size="B400">Remove</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Dialog>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
</SettingTile>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileDisplayName({ profile, userId }: ProfileProps) {
|
||||
const mx = useMatrixClient();
|
||||
const capabilities = useCapabilities();
|
||||
const disableSetDisplayname = capabilities['m.set_displayname']?.enabled === false;
|
||||
|
||||
const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
|
||||
const [displayName, setDisplayName] = useState<string>(defaultDisplayName);
|
||||
|
||||
const [changeState, changeDisplayName] = useAsyncCallback(
|
||||
useCallback((name: string) => mx.setDisplayName(name), [mx])
|
||||
);
|
||||
const changingDisplayName = changeState.status === AsyncStatus.Loading;
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(defaultDisplayName);
|
||||
}, [defaultDisplayName]);
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
||||
const name = evt.currentTarget.value;
|
||||
setDisplayName(name);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setDisplayName(defaultDisplayName);
|
||||
};
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
if (changingDisplayName) return;
|
||||
|
||||
const target = evt.target as HTMLFormElement | undefined;
|
||||
const displayNameInput = target?.displayNameInput as HTMLInputElement | undefined;
|
||||
const name = displayNameInput?.value;
|
||||
if (!name) return;
|
||||
|
||||
changeDisplayName(name);
|
||||
};
|
||||
|
||||
const hasChanges = displayName !== defaultDisplayName;
|
||||
return (
|
||||
<SettingTile
|
||||
title={
|
||||
<Text as="span" size="L400">
|
||||
Display Name
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Box direction="Column" grow="Yes" gap="100">
|
||||
<Box
|
||||
as="form"
|
||||
onSubmit={handleSubmit}
|
||||
gap="200"
|
||||
aria-disabled={changingDisplayName || disableSetDisplayname}
|
||||
>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Input
|
||||
required
|
||||
name="displayNameInput"
|
||||
value={displayName}
|
||||
onChange={handleChange}
|
||||
variant="Secondary"
|
||||
radii="300"
|
||||
style={{ paddingRight: config.space.S200 }}
|
||||
readOnly={changingDisplayName || disableSetDisplayname}
|
||||
after={
|
||||
hasChanges &&
|
||||
!changingDisplayName && (
|
||||
<IconButton
|
||||
type="reset"
|
||||
onClick={handleReset}
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Secondary"
|
||||
>
|
||||
<Icon src={Icons.Cross} size="100" />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
size="400"
|
||||
variant={hasChanges ? 'Success' : 'Secondary'}
|
||||
fill={hasChanges ? 'Solid' : 'Soft'}
|
||||
outlined
|
||||
radii="300"
|
||||
disabled={!hasChanges || changingDisplayName}
|
||||
type="submit"
|
||||
>
|
||||
{changingDisplayName && <Spinner variant="Success" fill="Solid" size="300" />}
|
||||
<Text size="B400">Save</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</SettingTile>
|
||||
);
|
||||
}
|
||||
|
||||
function Profile() {
|
||||
const mx = useMatrixClient();
|
||||
const userId = mx.getUserId()!;
|
||||
const profile = useUserProfile(userId);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Profile</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<ProfileAvatar userId={userId} profile={profile} />
|
||||
<ProfileDisplayName userId={userId} profile={profile} />
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ContactInformation() {
|
||||
const mx = useMatrixClient();
|
||||
const [threePIdsState, loadThreePIds] = useAsyncCallback(
|
||||
useCallback(() => mx.getThreePids(), [mx])
|
||||
);
|
||||
const threePIds =
|
||||
threePIdsState.status === AsyncStatus.Success ? threePIdsState.data.threepids : undefined;
|
||||
|
||||
const emailIds = threePIds?.filter((id) => id.medium === 'email');
|
||||
|
||||
useEffect(() => {
|
||||
loadThreePIds();
|
||||
}, [loadThreePIds]);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Contact Information</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile title="Email Address" description="Email address attached to your account.">
|
||||
<Box>
|
||||
{emailIds?.map((email) => (
|
||||
<Chip key={email.address} as="span" variant="Secondary" radii="Pill">
|
||||
<Text size="T200">{email.address}</Text>
|
||||
</Chip>
|
||||
))}
|
||||
</Box>
|
||||
{/* <Input defaultValue="" variant="Secondary" radii="300" /> */}
|
||||
</SettingTile>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
import { MatrixId } from './MatrixId';
|
||||
import { Profile } from './Profile';
|
||||
import { ContactInformation } from './ContactInfo';
|
||||
import { IgnoredUserList } from './IgnoredUserList';
|
||||
|
||||
type AccountProps = {
|
||||
requestClose: () => void;
|
||||
|
|
@ -419,6 +33,7 @@ export function Account({ requestClose }: AccountProps) {
|
|||
<Profile />
|
||||
<MatrixId />
|
||||
<ContactInformation />
|
||||
<IgnoredUserList />
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
|
|
|
|||
45
src/app/features/settings/account/ContactInfo.tsx
Normal file
45
src/app/features/settings/account/ContactInfo.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import { Box, Text, Chip } from 'folds';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
|
||||
export function ContactInformation() {
|
||||
const mx = useMatrixClient();
|
||||
const [threePIdsState, loadThreePIds] = useAsyncCallback(
|
||||
useCallback(() => mx.getThreePids(), [mx])
|
||||
);
|
||||
const threePIds =
|
||||
threePIdsState.status === AsyncStatus.Success ? threePIdsState.data.threepids : undefined;
|
||||
|
||||
const emailIds = threePIds?.filter((id) => id.medium === 'email');
|
||||
|
||||
useEffect(() => {
|
||||
loadThreePIds();
|
||||
}, [loadThreePIds]);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Contact Information</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile title="Email Address" description="Email address attached to your account.">
|
||||
<Box>
|
||||
{emailIds?.map((email) => (
|
||||
<Chip key={email.address} as="span" variant="Secondary" radii="Pill">
|
||||
<Text size="T200">{email.address}</Text>
|
||||
</Chip>
|
||||
))}
|
||||
</Box>
|
||||
{/* <Input defaultValue="" variant="Secondary" radii="300" /> */}
|
||||
</SettingTile>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,16 +7,17 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
|||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { isUserId } from '../../../utils/matrix';
|
||||
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
|
||||
import { useAlive } from '../../../hooks/useAlive';
|
||||
|
||||
function IgnoreUserInput({ userList }: { userList: string[] }) {
|
||||
const mx = useMatrixClient();
|
||||
const [userId, setUserId] = useState<string>('');
|
||||
const alive = useAlive();
|
||||
|
||||
const [ignoreState, ignore] = useAsyncCallback(
|
||||
useCallback(
|
||||
async (uId: string) => {
|
||||
mx.setIgnoredUsers([...userList, uId]);
|
||||
setUserId('');
|
||||
await mx.setIgnoredUsers([...userList, uId]);
|
||||
},
|
||||
[mx, userList]
|
||||
)
|
||||
|
|
@ -43,7 +44,11 @@ function IgnoreUserInput({ userList }: { userList: string[] }) {
|
|||
|
||||
if (!isUserId(uId)) return;
|
||||
|
||||
ignore(uId);
|
||||
ignore(uId).then(() => {
|
||||
if (alive()) {
|
||||
setUserId('');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -129,7 +134,7 @@ export function IgnoredUserList() {
|
|||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
||||
<Text size="L400">Block Messages</Text>
|
||||
<Text size="L400">Blocked Users</Text>
|
||||
</Box>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
|
|
@ -139,13 +144,13 @@ export function IgnoredUserList() {
|
|||
>
|
||||
<SettingTile
|
||||
title="Select User"
|
||||
description="Prevent receiving message by adding userId into blocklist."
|
||||
description="Prevent receiving messages or invites from user by adding their userId."
|
||||
>
|
||||
<Box direction="Column" gap="300">
|
||||
<IgnoreUserInput userList={ignoredUsers} />
|
||||
{ignoredUsers.length > 0 && (
|
||||
<Box direction="Inherit" gap="100">
|
||||
<Text size="L400">Blocklist</Text>
|
||||
<Text size="L400">Users</Text>
|
||||
<Box wrap="Wrap" gap="200">
|
||||
{ignoredUsers.map((userId) => (
|
||||
<IgnoredUserChip key={userId} userId={userId} userList={ignoredUsers} />
|
||||
33
src/app/features/settings/account/MatrixId.tsx
Normal file
33
src/app/features/settings/account/MatrixId.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import { Box, Text, Chip } from 'folds';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { copyToClipboard } from '../../../../util/common';
|
||||
|
||||
export function MatrixId() {
|
||||
const mx = useMatrixClient();
|
||||
const userId = mx.getUserId()!;
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Matrix ID</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={userId}
|
||||
after={
|
||||
<Chip variant="Secondary" radii="Pill" onClick={() => copyToClipboard(userId)}>
|
||||
<Text size="T200">Copy</Text>
|
||||
</Chip>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
325
src/app/features/settings/account/Profile.tsx
Normal file
325
src/app/features/settings/account/Profile.tsx
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
import React, {
|
||||
ChangeEventHandler,
|
||||
FormEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
IconButton,
|
||||
Icon,
|
||||
Icons,
|
||||
Input,
|
||||
Avatar,
|
||||
Button,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
Modal,
|
||||
Dialog,
|
||||
Header,
|
||||
config,
|
||||
Spinner,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
|
||||
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
||||
import { UserAvatar } from '../../../components/user-avatar';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { nameInitials } from '../../../utils/common';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useFilePicker } from '../../../hooks/useFilePicker';
|
||||
import { useObjectURL } from '../../../hooks/useObjectURL';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { ImageEditor } from '../../../components/image-editor';
|
||||
import { ModalWide } from '../../../styles/Modal.css';
|
||||
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
|
||||
import { CompactUploadCardRenderer } from '../../../components/upload-card';
|
||||
import { useCapabilities } from '../../../hooks/useCapabilities';
|
||||
|
||||
type ProfileProps = {
|
||||
profile: UserProfile;
|
||||
userId: string;
|
||||
};
|
||||
function ProfileAvatar({ profile, userId }: ProfileProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const capabilities = useCapabilities();
|
||||
const [alertRemove, setAlertRemove] = useState(false);
|
||||
const disableSetAvatar = capabilities['m.set_avatar_url']?.enabled === false;
|
||||
|
||||
const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
|
||||
const avatarUrl = profile.avatarUrl
|
||||
? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||
: undefined;
|
||||
|
||||
const [imageFile, setImageFile] = useState<File>();
|
||||
const imageFileURL = useObjectURL(imageFile);
|
||||
const uploadAtom = useMemo(() => {
|
||||
if (imageFile) return createUploadAtom(imageFile);
|
||||
return undefined;
|
||||
}, [imageFile]);
|
||||
|
||||
const pickFile = useFilePicker(setImageFile, false);
|
||||
|
||||
const handleRemoveUpload = useCallback(() => {
|
||||
setImageFile(undefined);
|
||||
}, []);
|
||||
|
||||
const handleUploaded = useCallback(
|
||||
(upload: UploadSuccess) => {
|
||||
const { mxc } = upload;
|
||||
mx.setAvatarUrl(mxc);
|
||||
handleRemoveUpload();
|
||||
},
|
||||
[mx, handleRemoveUpload]
|
||||
);
|
||||
|
||||
const handleRemoveAvatar = () => {
|
||||
mx.setAvatarUrl('');
|
||||
setAlertRemove(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingTile
|
||||
title={
|
||||
<Text as="span" size="L400">
|
||||
Avatar
|
||||
</Text>
|
||||
}
|
||||
after={
|
||||
<Avatar size="500" radii="300">
|
||||
<UserAvatar
|
||||
userId={userId}
|
||||
src={avatarUrl}
|
||||
renderFallback={() => <Text size="H4">{nameInitials(defaultDisplayName)}</Text>}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
{uploadAtom ? (
|
||||
<Box gap="200" direction="Column">
|
||||
<CompactUploadCardRenderer
|
||||
uploadAtom={uploadAtom}
|
||||
onRemove={handleRemoveUpload}
|
||||
onComplete={handleUploaded}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box gap="200">
|
||||
<Button
|
||||
onClick={() => pickFile('image/*')}
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
outlined
|
||||
radii="300"
|
||||
disabled={disableSetAvatar}
|
||||
>
|
||||
<Text size="B300">Upload</Text>
|
||||
</Button>
|
||||
{avatarUrl && (
|
||||
<Button
|
||||
size="300"
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
radii="300"
|
||||
disabled={disableSetAvatar}
|
||||
onClick={() => setAlertRemove(true)}
|
||||
>
|
||||
<Text size="B300">Remove</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{imageFileURL && (
|
||||
<Overlay open={false} backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: handleRemoveUpload,
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Modal className={ModalWide} variant="Surface" size="500">
|
||||
<ImageEditor
|
||||
name={imageFile?.name ?? 'Unnamed'}
|
||||
url={imageFileURL}
|
||||
requestClose={handleRemoveUpload}
|
||||
/>
|
||||
</Modal>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
)}
|
||||
|
||||
<Overlay open={alertRemove} backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setAlertRemove(false),
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Dialog variant="Surface">
|
||||
<Header
|
||||
style={{
|
||||
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
}}
|
||||
variant="Surface"
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text size="H4">Remove Avatar</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={() => setAlertRemove(false)} radii="300">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||
<Box direction="Column" gap="200">
|
||||
<Text priority="400">Are you sure you want to remove profile avatar?</Text>
|
||||
</Box>
|
||||
<Button variant="Critical" onClick={handleRemoveAvatar}>
|
||||
<Text size="B400">Remove</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Dialog>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
</SettingTile>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileDisplayName({ profile, userId }: ProfileProps) {
|
||||
const mx = useMatrixClient();
|
||||
const capabilities = useCapabilities();
|
||||
const disableSetDisplayname = capabilities['m.set_displayname']?.enabled === false;
|
||||
|
||||
const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
|
||||
const [displayName, setDisplayName] = useState<string>(defaultDisplayName);
|
||||
|
||||
const [changeState, changeDisplayName] = useAsyncCallback(
|
||||
useCallback((name: string) => mx.setDisplayName(name), [mx])
|
||||
);
|
||||
const changingDisplayName = changeState.status === AsyncStatus.Loading;
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(defaultDisplayName);
|
||||
}, [defaultDisplayName]);
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
||||
const name = evt.currentTarget.value;
|
||||
setDisplayName(name);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setDisplayName(defaultDisplayName);
|
||||
};
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
if (changingDisplayName) return;
|
||||
|
||||
const target = evt.target as HTMLFormElement | undefined;
|
||||
const displayNameInput = target?.displayNameInput as HTMLInputElement | undefined;
|
||||
const name = displayNameInput?.value;
|
||||
if (!name) return;
|
||||
|
||||
changeDisplayName(name);
|
||||
};
|
||||
|
||||
const hasChanges = displayName !== defaultDisplayName;
|
||||
return (
|
||||
<SettingTile
|
||||
title={
|
||||
<Text as="span" size="L400">
|
||||
Display Name
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Box direction="Column" grow="Yes" gap="100">
|
||||
<Box
|
||||
as="form"
|
||||
onSubmit={handleSubmit}
|
||||
gap="200"
|
||||
aria-disabled={changingDisplayName || disableSetDisplayname}
|
||||
>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Input
|
||||
required
|
||||
name="displayNameInput"
|
||||
value={displayName}
|
||||
onChange={handleChange}
|
||||
variant="Secondary"
|
||||
radii="300"
|
||||
style={{ paddingRight: config.space.S200 }}
|
||||
readOnly={changingDisplayName || disableSetDisplayname}
|
||||
after={
|
||||
hasChanges &&
|
||||
!changingDisplayName && (
|
||||
<IconButton
|
||||
type="reset"
|
||||
onClick={handleReset}
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Secondary"
|
||||
>
|
||||
<Icon src={Icons.Cross} size="100" />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
size="400"
|
||||
variant={hasChanges ? 'Success' : 'Secondary'}
|
||||
fill={hasChanges ? 'Solid' : 'Soft'}
|
||||
outlined
|
||||
radii="300"
|
||||
disabled={!hasChanges || changingDisplayName}
|
||||
type="submit"
|
||||
>
|
||||
{changingDisplayName && <Spinner variant="Success" fill="Solid" size="300" />}
|
||||
<Text size="B400">Save</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</SettingTile>
|
||||
);
|
||||
}
|
||||
|
||||
export function Profile() {
|
||||
const mx = useMatrixClient();
|
||||
const userId = mx.getUserId()!;
|
||||
const profile = useUserProfile(userId);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Profile</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<ProfileAvatar userId={userId} profile={profile} />
|
||||
<ProfileDisplayName userId={userId} profile={profile} />
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@ import { SystemNotification } from './SystemNotification';
|
|||
import { AllMessagesNotifications } from './AllMessages';
|
||||
import { SpecialMessagesNotifications } from './SpecialMessages';
|
||||
import { KeywordMessagesNotifications } from './KeywordMessages';
|
||||
import { IgnoredUserList } from './IgnoredUserList';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
|
||||
type NotificationsProps = {
|
||||
requestClose: () => void;
|
||||
|
|
@ -35,7 +37,19 @@ export function Notifications({ requestClose }: NotificationsProps) {
|
|||
<AllMessagesNotifications />
|
||||
<SpecialMessagesNotifications />
|
||||
<KeywordMessagesNotifications />
|
||||
<IgnoredUserList />
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Block Messages</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
description={'This option has been moved to "Account > Block Users" section.'}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
|
|
|
|||
|
|
@ -1,34 +1,127 @@
|
|||
import { MatrixClient, Room } from 'matrix-js-sdk';
|
||||
import { Direction, IContextResponse, MatrixClient, Method, Room, RoomMember } from 'matrix-js-sdk';
|
||||
import { RoomServerAclEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import { useMemo } from 'react';
|
||||
import { getDMRoomFor, isRoomAlias, isRoomId, isUserId } from '../utils/matrix';
|
||||
import {
|
||||
getDMRoomFor,
|
||||
isRoomAlias,
|
||||
isRoomId,
|
||||
isServerName,
|
||||
isUserId,
|
||||
rateLimitedActions,
|
||||
} from '../utils/matrix';
|
||||
import { hasDevices } from '../../util/matrixUtil';
|
||||
import * as roomActions from '../../client/action/room';
|
||||
import { useRoomNavigate } from './useRoomNavigate';
|
||||
import { Membership, StateEvent } from '../../types/matrix/room';
|
||||
import { getStateEvent } from '../utils/room';
|
||||
import { splitWithSpace } from '../utils/common';
|
||||
|
||||
export const SHRUG = '¯\\_(ツ)_/¯';
|
||||
export const TABLEFLIP = '(╯°□°)╯︵ ┻━┻';
|
||||
export const UNFLIP = '┬─┬ノ( º_ºノ)';
|
||||
|
||||
export function parseUsersAndReason(payload: string): {
|
||||
users: string[];
|
||||
reason?: string;
|
||||
} {
|
||||
let reason: string | undefined;
|
||||
let ids: string = payload;
|
||||
const FLAG_PAT = '(?:^|\\s)-(\\w+)\\b';
|
||||
const FLAG_REG = new RegExp(FLAG_PAT);
|
||||
const FLAG_REG_G = new RegExp(FLAG_PAT, 'g');
|
||||
|
||||
const reasonMatch = payload.match(/\s-r\s/);
|
||||
if (reasonMatch) {
|
||||
ids = payload.slice(0, reasonMatch.index);
|
||||
reason = payload.slice((reasonMatch.index ?? 0) + reasonMatch[0].length);
|
||||
if (reason.trim() === '') reason = undefined;
|
||||
export const splitPayloadContentAndFlags = (payload: string): [string, string | undefined] => {
|
||||
const flagMatch = payload.match(FLAG_REG);
|
||||
|
||||
if (!flagMatch) {
|
||||
return [payload, undefined];
|
||||
}
|
||||
const rawIds = ids.split(' ');
|
||||
const users = rawIds.filter((id) => isUserId(id));
|
||||
return {
|
||||
users,
|
||||
reason,
|
||||
};
|
||||
}
|
||||
const content = payload.slice(0, flagMatch.index);
|
||||
const flags = payload.slice(flagMatch.index);
|
||||
|
||||
return [content, flags];
|
||||
};
|
||||
|
||||
export const parseFlags = (flags: string | undefined): Record<string, string | undefined> => {
|
||||
const result: Record<string, string> = {};
|
||||
if (!flags) return result;
|
||||
|
||||
const matches: { key: string; index: number; match: string }[] = [];
|
||||
|
||||
for (let match = FLAG_REG_G.exec(flags); match !== null; match = FLAG_REG_G.exec(flags)) {
|
||||
matches.push({ key: match[1], index: match.index, match: match[0] });
|
||||
}
|
||||
|
||||
for (let i = 0; i < matches.length; i += 1) {
|
||||
const { key, match } = matches[i];
|
||||
const start = matches[i].index + match.length;
|
||||
const end = i + 1 < matches.length ? matches[i + 1].index : flags.length;
|
||||
const value = flags.slice(start, end).trim();
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const parseUsers = (payload: string): string[] => {
|
||||
const users: string[] = [];
|
||||
|
||||
splitWithSpace(payload).forEach((item) => {
|
||||
if (isUserId(item)) {
|
||||
users.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return users;
|
||||
};
|
||||
|
||||
export const parseServers = (payload: string): string[] => {
|
||||
const servers: string[] = [];
|
||||
|
||||
splitWithSpace(payload).forEach((item) => {
|
||||
if (isServerName(item)) {
|
||||
servers.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return servers;
|
||||
};
|
||||
|
||||
const getServerMembers = (room: Room, server: string): RoomMember[] => {
|
||||
const members: RoomMember[] = room
|
||||
.getMembers()
|
||||
.filter((member) => member.userId.endsWith(`:${server}`));
|
||||
|
||||
return members;
|
||||
};
|
||||
|
||||
export const parseTimestampFlag = (input: string): number | undefined => {
|
||||
const match = input.match(/^(\d+(?:\.\d+)?)([dhms])$/); // supports floats like 1.5d
|
||||
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const value = parseFloat(match[1]); // supports decimal values
|
||||
const unit = match[2];
|
||||
|
||||
const now = Date.now(); // in milliseconds
|
||||
let delta = 0;
|
||||
|
||||
switch (unit) {
|
||||
case 'd':
|
||||
delta = value * 24 * 60 * 60 * 1000;
|
||||
break;
|
||||
case 'h':
|
||||
delta = value * 60 * 60 * 1000;
|
||||
break;
|
||||
case 'm':
|
||||
delta = value * 60 * 1000;
|
||||
break;
|
||||
case 's':
|
||||
delta = value * 1000;
|
||||
break;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const timestamp = now - delta;
|
||||
return timestamp;
|
||||
};
|
||||
|
||||
export type CommandExe = (payload: string) => Promise<void>;
|
||||
|
||||
|
|
@ -52,6 +145,8 @@ export enum Command {
|
|||
ConvertToRoom = 'converttoroom',
|
||||
TableFlip = 'tableflip',
|
||||
UnFlip = 'unflip',
|
||||
Delete = 'delete',
|
||||
Acl = 'acl',
|
||||
}
|
||||
|
||||
export type CommandContent = {
|
||||
|
|
@ -96,7 +191,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.StartDm,
|
||||
description: 'Start direct message with user. Example: /startdm userId1',
|
||||
exe: async (payload) => {
|
||||
const rawIds = payload.split(' ');
|
||||
const rawIds = splitWithSpace(payload);
|
||||
const userIds = rawIds.filter((id) => isUserId(id) && id !== mx.getUserId());
|
||||
if (userIds.length === 0) return;
|
||||
if (userIds.length === 1) {
|
||||
|
|
@ -106,7 +201,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
return;
|
||||
}
|
||||
}
|
||||
const devices = await Promise.all(userIds.map(uid => hasDevices(mx, uid)));
|
||||
const devices = await Promise.all(userIds.map((uid) => hasDevices(mx, uid)));
|
||||
const isEncrypt = devices.every((hasDevice) => hasDevice);
|
||||
const result = await roomActions.createDM(mx, userIds, isEncrypt);
|
||||
navigateRoom(result.room_id);
|
||||
|
|
@ -116,7 +211,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.Join,
|
||||
description: 'Join room with address. Example: /join address1 address2',
|
||||
exe: async (payload) => {
|
||||
const rawIds = payload.split(' ');
|
||||
const rawIds = splitWithSpace(payload);
|
||||
const roomIds = rawIds.filter(
|
||||
(idOrAlias) => isRoomId(idOrAlias) || isRoomAlias(idOrAlias)
|
||||
);
|
||||
|
|
@ -131,7 +226,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
mx.leave(room.roomId);
|
||||
return;
|
||||
}
|
||||
const rawIds = payload.split(' ');
|
||||
const rawIds = splitWithSpace(payload);
|
||||
const roomIds = rawIds.filter((id) => isRoomId(id));
|
||||
roomIds.map((id) => mx.leave(id));
|
||||
},
|
||||
|
|
@ -140,7 +235,10 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.Invite,
|
||||
description: 'Invite user to room. Example: /invite userId1 userId2 [-r reason]',
|
||||
exe: async (payload) => {
|
||||
const { users, reason } = parseUsersAndReason(payload);
|
||||
const [content, flags] = splitPayloadContentAndFlags(payload);
|
||||
const users = parseUsers(content);
|
||||
const flagToContent = parseFlags(flags);
|
||||
const reason = flagToContent.r;
|
||||
users.map((id) => mx.invite(room.roomId, id, reason));
|
||||
},
|
||||
},
|
||||
|
|
@ -148,31 +246,64 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.DisInvite,
|
||||
description: 'Disinvite user to room. Example: /disinvite userId1 userId2 [-r reason]',
|
||||
exe: async (payload) => {
|
||||
const { users, reason } = parseUsersAndReason(payload);
|
||||
const [content, flags] = splitPayloadContentAndFlags(payload);
|
||||
const users = parseUsers(content);
|
||||
const flagToContent = parseFlags(flags);
|
||||
const reason = flagToContent.r;
|
||||
users.map((id) => mx.kick(room.roomId, id, reason));
|
||||
},
|
||||
},
|
||||
[Command.Kick]: {
|
||||
name: Command.Kick,
|
||||
description: 'Kick user from room. Example: /kick userId1 userId2 [-r reason]',
|
||||
description: 'Kick user from room. Example: /kick userId1 userId2 servername [-r reason]',
|
||||
exe: async (payload) => {
|
||||
const { users, reason } = parseUsersAndReason(payload);
|
||||
users.map((id) => mx.kick(room.roomId, id, reason));
|
||||
const [content, flags] = splitPayloadContentAndFlags(payload);
|
||||
const users = parseUsers(content);
|
||||
const servers = parseServers(content);
|
||||
const flagToContent = parseFlags(flags);
|
||||
const reason = flagToContent.r;
|
||||
|
||||
const serverMembers = servers?.flatMap((server) => getServerMembers(room, server));
|
||||
const serverUsers = serverMembers
|
||||
?.filter((m) => m.membership !== Membership.Ban)
|
||||
.map((m) => m.userId);
|
||||
|
||||
if (Array.isArray(serverUsers)) {
|
||||
serverUsers.forEach((user) => {
|
||||
if (!users.includes(user)) users.push(user);
|
||||
});
|
||||
}
|
||||
|
||||
rateLimitedActions(users, (id) => mx.kick(room.roomId, id, reason));
|
||||
},
|
||||
},
|
||||
[Command.Ban]: {
|
||||
name: Command.Ban,
|
||||
description: 'Ban user from room. Example: /ban userId1 userId2 [-r reason]',
|
||||
description: 'Ban user from room. Example: /ban userId1 userId2 servername [-r reason]',
|
||||
exe: async (payload) => {
|
||||
const { users, reason } = parseUsersAndReason(payload);
|
||||
users.map((id) => mx.ban(room.roomId, id, reason));
|
||||
const [content, flags] = splitPayloadContentAndFlags(payload);
|
||||
const users = parseUsers(content);
|
||||
const servers = parseServers(content);
|
||||
const flagToContent = parseFlags(flags);
|
||||
const reason = flagToContent.r;
|
||||
|
||||
const serverMembers = servers?.flatMap((server) => getServerMembers(room, server));
|
||||
const serverUsers = serverMembers?.map((m) => m.userId);
|
||||
|
||||
if (Array.isArray(serverUsers)) {
|
||||
serverUsers.forEach((user) => {
|
||||
if (!users.includes(user)) users.push(user);
|
||||
});
|
||||
}
|
||||
|
||||
rateLimitedActions(users, (id) => mx.ban(room.roomId, id, reason));
|
||||
},
|
||||
},
|
||||
[Command.UnBan]: {
|
||||
name: Command.UnBan,
|
||||
description: 'Unban user from room. Example: /unban userId1 userId2',
|
||||
exe: async (payload) => {
|
||||
const rawIds = payload.split(' ');
|
||||
const rawIds = splitWithSpace(payload);
|
||||
const users = rawIds.filter((id) => isUserId(id));
|
||||
users.map((id) => mx.unban(room.roomId, id));
|
||||
},
|
||||
|
|
@ -181,7 +312,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.Ignore,
|
||||
description: 'Ignore user. Example: /ignore userId1 userId2',
|
||||
exe: async (payload) => {
|
||||
const rawIds = payload.split(' ');
|
||||
const rawIds = splitWithSpace(payload);
|
||||
const userIds = rawIds.filter((id) => isUserId(id));
|
||||
if (userIds.length > 0) roomActions.ignore(mx, userIds);
|
||||
},
|
||||
|
|
@ -190,7 +321,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
name: Command.UnIgnore,
|
||||
description: 'Unignore user. Example: /unignore userId1 userId2',
|
||||
exe: async (payload) => {
|
||||
const rawIds = payload.split(' ');
|
||||
const rawIds = splitWithSpace(payload);
|
||||
const userIds = rawIds.filter((id) => isUserId(id));
|
||||
if (userIds.length > 0) roomActions.unignore(mx, userIds);
|
||||
},
|
||||
|
|
@ -227,6 +358,124 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
roomActions.convertToRoom(mx, room.roomId);
|
||||
},
|
||||
},
|
||||
[Command.Delete]: {
|
||||
name: Command.Delete,
|
||||
description:
|
||||
'Delete messages from users. Example: /delete userId1 servername -past 1d|2h|5m|30s [-t m.room.message] [-r spam]',
|
||||
exe: async (payload) => {
|
||||
const [content, flags] = splitPayloadContentAndFlags(payload);
|
||||
const users = parseUsers(content);
|
||||
const servers = parseServers(content);
|
||||
|
||||
const flagToContent = parseFlags(flags);
|
||||
const reason = flagToContent.r;
|
||||
const pastContent = flagToContent.past ?? '';
|
||||
const msgTypeContent = flagToContent.t;
|
||||
const messageTypes: string[] = msgTypeContent ? splitWithSpace(msgTypeContent) : [];
|
||||
|
||||
const ts = parseTimestampFlag(pastContent);
|
||||
if (!ts) return;
|
||||
|
||||
const serverMembers = servers?.flatMap((server) => getServerMembers(room, server));
|
||||
const serverUsers = serverMembers?.map((m) => m.userId);
|
||||
|
||||
if (Array.isArray(serverUsers)) {
|
||||
serverUsers.forEach((user) => {
|
||||
if (!users.includes(user)) users.push(user);
|
||||
});
|
||||
}
|
||||
|
||||
const result = await mx.timestampToEvent(room.roomId, ts, Direction.Forward);
|
||||
const startEventId = result.event_id;
|
||||
|
||||
const path = `/rooms/${encodeURIComponent(room.roomId)}/context/${encodeURIComponent(
|
||||
startEventId
|
||||
)}`;
|
||||
const eventContext = await mx.http.authedRequest<IContextResponse>(Method.Get, path, {
|
||||
limit: 0,
|
||||
});
|
||||
|
||||
let token: string | undefined = eventContext.start;
|
||||
while (token) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await mx.createMessagesRequest(
|
||||
room.roomId,
|
||||
token,
|
||||
20,
|
||||
Direction.Forward,
|
||||
undefined
|
||||
);
|
||||
const { end, chunk } = response;
|
||||
// remove until the latest event;
|
||||
token = end;
|
||||
|
||||
const eventsToDelete = chunk.filter(
|
||||
(roomEvent) =>
|
||||
(messageTypes.length > 0 ? messageTypes.includes(roomEvent.type) : true) &&
|
||||
users.includes(roomEvent.sender) &&
|
||||
roomEvent.unsigned?.redacted_because === undefined
|
||||
);
|
||||
|
||||
const eventIds = eventsToDelete.map((roomEvent) => roomEvent.event_id);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await rateLimitedActions(eventIds, (eventId) =>
|
||||
mx.redactEvent(room.roomId, eventId, undefined, { reason })
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
[Command.Acl]: {
|
||||
name: Command.Acl,
|
||||
description:
|
||||
'Manage server access control list. Example /acl [-a servername1] [-d servername2] [-ra servername1] [-rd servername2]',
|
||||
exe: async (payload) => {
|
||||
const [, flags] = splitPayloadContentAndFlags(payload);
|
||||
|
||||
const flagToContent = parseFlags(flags);
|
||||
const allowFlag = flagToContent.a;
|
||||
const denyFlag = flagToContent.d;
|
||||
const removeAllowFlag = flagToContent.ra;
|
||||
const removeDenyFlag = flagToContent.rd;
|
||||
|
||||
const allowList = allowFlag ? splitWithSpace(allowFlag) : [];
|
||||
const denyList = denyFlag ? splitWithSpace(denyFlag) : [];
|
||||
const removeAllowList = removeAllowFlag ? splitWithSpace(removeAllowFlag) : [];
|
||||
const removeDenyList = removeDenyFlag ? splitWithSpace(removeDenyFlag) : [];
|
||||
|
||||
const serverAcl = getStateEvent(
|
||||
room,
|
||||
StateEvent.RoomServerAcl
|
||||
)?.getContent<RoomServerAclEventContent>();
|
||||
|
||||
const aclContent: RoomServerAclEventContent = {
|
||||
allow: serverAcl?.allow ? [...serverAcl.allow] : [],
|
||||
allow_ip_literals: serverAcl?.allow_ip_literals,
|
||||
deny: serverAcl?.deny ? [...serverAcl.deny] : [],
|
||||
};
|
||||
|
||||
allowList.forEach((servername) => {
|
||||
if (!Array.isArray(aclContent.allow) || aclContent.allow.includes(servername)) return;
|
||||
aclContent.allow.push(servername);
|
||||
});
|
||||
denyList.forEach((servername) => {
|
||||
if (!Array.isArray(aclContent.deny) || aclContent.deny.includes(servername)) return;
|
||||
aclContent.deny.push(servername);
|
||||
});
|
||||
|
||||
aclContent.allow = aclContent.allow?.filter(
|
||||
(servername) => !removeAllowList.includes(servername)
|
||||
);
|
||||
aclContent.deny = aclContent.deny?.filter(
|
||||
(servername) => !removeDenyList.includes(servername)
|
||||
);
|
||||
|
||||
aclContent.allow?.sort();
|
||||
aclContent.deny?.sort();
|
||||
|
||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomServerAcl as any, aclContent);
|
||||
},
|
||||
},
|
||||
}),
|
||||
[mx, room, navigateRoom]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { IMyDevice } from 'matrix-js-sdk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { CryptoEvent, CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto';
|
||||
import { CryptoEvent, CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
export const useDeviceListChange = (
|
||||
|
|
|
|||
10
src/app/hooks/useReportRoomSupported.ts
Normal file
10
src/app/hooks/useReportRoomSupported.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { useSpecVersions } from './useSpecVersions';
|
||||
|
||||
export const useReportRoomSupported = (): boolean => {
|
||||
const { versions, unstable_features: unstableFeatures } = useSpecVersions();
|
||||
|
||||
// report room is introduced in spec version 1.13
|
||||
const supported = unstableFeatures?.['org.matrix.msc4151'] || versions.includes('v1.13');
|
||||
|
||||
return supported;
|
||||
};
|
||||
|
|
@ -13,6 +13,8 @@ import { getOrphanParents } from '../utils/room';
|
|||
import { roomToParentsAtom } from '../state/room/roomToParents';
|
||||
import { mDirectAtom } from '../state/mDirectList';
|
||||
import { useSelectedSpace } from './router/useSelectedSpace';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
|
||||
export const useRoomNavigate = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -20,6 +22,7 @@ export const useRoomNavigate = () => {
|
|||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const spaceSelectedId = useSelectedSpace();
|
||||
const [developerTools] = useSetting(settingsAtom, 'developerTools');
|
||||
|
||||
const navigateSpace = useCallback(
|
||||
(roomId: string) => {
|
||||
|
|
@ -32,15 +35,22 @@ export const useRoomNavigate = () => {
|
|||
const navigateRoom = useCallback(
|
||||
(roomId: string, eventId?: string, opts?: NavigateOptions) => {
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
|
||||
const openSpaceTimeline = developerTools && spaceSelectedId === roomId;
|
||||
|
||||
const orphanParents = getOrphanParents(roomToParents, roomId);
|
||||
const orphanParents = openSpaceTimeline ? [roomId] : getOrphanParents(roomToParents, roomId);
|
||||
if (orphanParents.length > 0) {
|
||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(
|
||||
mx,
|
||||
spaceSelectedId && orphanParents.includes(spaceSelectedId)
|
||||
? spaceSelectedId
|
||||
: orphanParents[0]
|
||||
: orphanParents[0] // TODO: better orphan parent selection.
|
||||
);
|
||||
|
||||
if (openSpaceTimeline) {
|
||||
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomId, eventId), opts);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
|
||||
return;
|
||||
}
|
||||
|
|
@ -52,7 +62,7 @@ export const useRoomNavigate = () => {
|
|||
|
||||
navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
|
||||
},
|
||||
[mx, navigate, spaceSelectedId, roomToParents, mDirects]
|
||||
[mx, navigate, spaceSelectedId, roomToParents, mDirects, developerTools]
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -28,9 +28,11 @@ import { allRoomsAtom } from '../../state/room-list/roomList';
|
|||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { getViaServers } from '../../plugins/via-servers';
|
||||
import { rateLimitedActions } from '../../utils/matrix';
|
||||
import { useAlive } from '../../hooks/useAlive';
|
||||
|
||||
function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
|
||||
const mountStore = useStore(roomId);
|
||||
const alive = useAlive();
|
||||
const [debounce] = useState(new Debounce());
|
||||
const [process, setProcess] = useState(null);
|
||||
const [allRoomIds, setAllRoomIds] = useState([]);
|
||||
|
|
@ -68,14 +70,14 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
|
|||
const handleAdd = async () => {
|
||||
setProcess(`Adding ${selected.length} items...`);
|
||||
|
||||
const promises = selected.map((rId) => {
|
||||
await rateLimitedActions(selected, async (rId) => {
|
||||
const room = mx.getRoom(rId);
|
||||
const via = getViaServers(room);
|
||||
if (via.length === 0) {
|
||||
via.push(getIdServer(rId));
|
||||
}
|
||||
|
||||
return mx.sendStateEvent(
|
||||
await mx.sendStateEvent(
|
||||
roomId,
|
||||
'm.space.child',
|
||||
{
|
||||
|
|
@ -87,9 +89,7 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
|
|||
);
|
||||
});
|
||||
|
||||
mountStore.setItem(true);
|
||||
await Promise.allSettled(promises);
|
||||
if (mountStore.getItem() !== true) return;
|
||||
if (!alive()) return;
|
||||
|
||||
const roomIds = onlySpaces ? [...spaces] : [...rooms, ...directs];
|
||||
const allIds = roomIds.filter(
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) {
|
|||
searchUser(usernameRef.current.value);
|
||||
}}
|
||||
>
|
||||
<Input value={searchTerm} forwardRef={usernameRef} label="Name or userId" />
|
||||
<Input value={searchTerm} forwardRef={usernameRef} label="Name or userId" autoFocus />
|
||||
<Button disabled={isSearching} iconSrc={UserIC} variant="primary" type="submit">
|
||||
Search
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ function JoinAliasContent({ term, requestClose }) {
|
|||
|
||||
return (
|
||||
<form className="join-alias" onSubmit={handleSubmit}>
|
||||
<Input label="Address" value={term} name="alias" required />
|
||||
<Input label="Address" value={term} name="alias" required autoFocus />
|
||||
{error && (
|
||||
<Text className="join-alias__error" variant="b3">
|
||||
{error}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function AuthFooter() {
|
|||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
v4.6.0
|
||||
v4.8.1
|
||||
</Text>
|
||||
<Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
|
||||
Twitter
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export const login = async (
|
|||
}
|
||||
|
||||
const mx = createClient({ baseUrl: url });
|
||||
const [err, res] = await to<LoginResponse, MatrixError>(mx.login(data.type, data));
|
||||
const [err, res] = await to<LoginResponse, MatrixError>(mx.loginRequest(data));
|
||||
|
||||
if (err) {
|
||||
if (err.httpStatus === 400) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function WelcomePage() {
|
|||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
v4.6.0
|
||||
v4.8.1
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ export function Explore() {
|
|||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
||||
<Avatar size="200" radii="400">
|
||||
<Icon
|
||||
src={Icons.Category}
|
||||
src={Icons.Server}
|
||||
size="100"
|
||||
filled={selectedServer === userServer}
|
||||
/>
|
||||
|
|
@ -243,11 +243,7 @@ export function Explore() {
|
|||
<NavItemContent>
|
||||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
||||
<Avatar size="200" radii="400">
|
||||
<Icon
|
||||
src={Icons.Category}
|
||||
size="100"
|
||||
filled={server === selectedServer}
|
||||
/>
|
||||
<Icon src={Icons.Server} size="100" filled={server === selectedServer} />
|
||||
</Avatar>
|
||||
<Box as="span" grow="Yes">
|
||||
<Text as="span" size="Inherit" truncate>
|
||||
|
|
|
|||
|
|
@ -507,7 +507,7 @@ export function PublicRooms() {
|
|||
)}
|
||||
</Box>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Category} />}
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Server} />}
|
||||
<Text size="H3" truncate>
|
||||
{server}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ function InvitesNavItem() {
|
|||
</Avatar>
|
||||
<Box as="span" grow="Yes">
|
||||
<Text as="span" size="Inherit" truncate>
|
||||
Invitations
|
||||
Invites
|
||||
</Text>
|
||||
</Box>
|
||||
{inviteCount > 0 && <UnreadBadge highlight count={inviteCount} />}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
|
|
@ -16,56 +18,129 @@ import {
|
|||
config,
|
||||
} from 'folds';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { RoomTopicEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { MatrixError, Room } from 'matrix-js-sdk';
|
||||
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
|
||||
import { useDirectInvites, useRoomInvites, useSpaceInvites } from '../../../state/hooks/inviteList';
|
||||
import { MatrixClient, MatrixError, Room } from 'matrix-js-sdk';
|
||||
import {
|
||||
Page,
|
||||
PageContent,
|
||||
PageContentCenter,
|
||||
PageHeader,
|
||||
PageHero,
|
||||
PageHeroEmpty,
|
||||
PageHeroSection,
|
||||
} from '../../../components/page';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { allInvitesAtom } from '../../../state/room-list/inviteList';
|
||||
import { mDirectAtom } from '../../../state/mDirectList';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import {
|
||||
bannedInRooms,
|
||||
getCommonRooms,
|
||||
getDirectRoomAvatarUrl,
|
||||
getMemberDisplayName,
|
||||
getRoomAvatarUrl,
|
||||
getStateEvent,
|
||||
isDirectInvite,
|
||||
isSpace,
|
||||
} from '../../../utils/room';
|
||||
import { nameInitials } from '../../../utils/common';
|
||||
import { RoomAvatar } from '../../../components/room-avatar';
|
||||
import { addRoomIdToMDirect, getMxIdLocalPart, guessDmRoomUserId } from '../../../utils/matrix';
|
||||
import {
|
||||
addRoomIdToMDirect,
|
||||
getMxIdLocalPart,
|
||||
guessDmRoomUserId,
|
||||
rateLimitedActions,
|
||||
} from '../../../utils/matrix';
|
||||
import { Time } from '../../../components/message';
|
||||
import { useElementSizeObserver } from '../../../hooks/useElementSizeObserver';
|
||||
import { onEnterOrSpace, stopPropagation } from '../../../utils/keyboard';
|
||||
import { RoomTopicViewer } from '../../../components/room-topic-viewer';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||
import { useRoomTopic } from '../../../hooks/useRoomMeta';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { testBadWords } from '../../../plugins/bad-words';
|
||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
|
||||
import { useReportRoomSupported } from '../../../hooks/useReportRoomSupported';
|
||||
|
||||
const COMPACT_CARD_WIDTH = 548;
|
||||
|
||||
type InviteCardProps = {
|
||||
type InviteData = {
|
||||
room: Room;
|
||||
userId: string;
|
||||
direct?: boolean;
|
||||
compact?: boolean;
|
||||
onNavigate: (roomId: string) => void;
|
||||
roomId: string;
|
||||
roomName: string;
|
||||
roomAvatar?: string;
|
||||
roomTopic?: string;
|
||||
roomAlias?: string;
|
||||
|
||||
senderId: string;
|
||||
senderName: string;
|
||||
inviteTs?: number;
|
||||
|
||||
isSpace: boolean;
|
||||
isDirect: boolean;
|
||||
isEncrypted: boolean;
|
||||
};
|
||||
function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
|
||||
const makeInviteData = (mx: MatrixClient, room: Room, useAuthentication: boolean): InviteData => {
|
||||
const userId = mx.getSafeUserId();
|
||||
const direct = isDirectInvite(room, userId);
|
||||
|
||||
const roomAvatar = direct
|
||||
? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||
: getRoomAvatarUrl(mx, room, 96, useAuthentication);
|
||||
const roomName = room.name || room.getCanonicalAlias() || room.roomId;
|
||||
const roomTopic =
|
||||
getStateEvent(room, StateEvent.RoomTopic)?.getContent<RoomTopicEventContent>()?.topic ??
|
||||
undefined;
|
||||
|
||||
const member = room.getMember(userId);
|
||||
const memberEvent = member?.events.member;
|
||||
const memberTs = memberEvent?.getTs() ?? 0;
|
||||
|
||||
const senderId = memberEvent?.getSender();
|
||||
const senderName = senderId
|
||||
? getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId
|
||||
: undefined;
|
||||
const inviteTs = memberEvent?.getTs() ?? 0;
|
||||
|
||||
const topic = useRoomTopic(room);
|
||||
return {
|
||||
room,
|
||||
roomId: room.roomId,
|
||||
roomAvatar,
|
||||
roomName,
|
||||
roomTopic,
|
||||
roomAlias: room.getCanonicalAlias() ?? undefined,
|
||||
|
||||
senderId: senderId ?? 'Unknown',
|
||||
senderName: senderName ?? 'Unknown',
|
||||
inviteTs,
|
||||
|
||||
isSpace: isSpace(room),
|
||||
isDirect: direct,
|
||||
isEncrypted: !!getStateEvent(room, StateEvent.RoomEncryption),
|
||||
};
|
||||
};
|
||||
|
||||
const hasBadWords = (invite: InviteData): boolean =>
|
||||
testBadWords(invite.roomName) ||
|
||||
testBadWords(invite.roomTopic ?? '') ||
|
||||
testBadWords(invite.senderName) ||
|
||||
testBadWords(invite.senderId);
|
||||
|
||||
type NavigateHandler = (roomId: string, space: boolean) => void;
|
||||
|
||||
type InviteCardProps = {
|
||||
invite: InviteData;
|
||||
compact?: boolean;
|
||||
onNavigate: NavigateHandler;
|
||||
hideAvatar: boolean;
|
||||
};
|
||||
function InviteCard({ invite, compact, onNavigate, hideAvatar }: InviteCardProps) {
|
||||
const mx = useMatrixClient();
|
||||
const userId = mx.getSafeUserId();
|
||||
|
||||
const [viewTopic, setViewTopic] = useState(false);
|
||||
const closeTopic = () => setViewTopic(false);
|
||||
|
|
@ -73,17 +148,19 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
|||
|
||||
const [joinState, join] = useAsyncCallback<void, MatrixError, []>(
|
||||
useCallback(async () => {
|
||||
const dmUserId = isDirectInvite(room, userId) ? guessDmRoomUserId(room, userId) : undefined;
|
||||
const dmUserId = isDirectInvite(invite.room, userId)
|
||||
? guessDmRoomUserId(invite.room, userId)
|
||||
: undefined;
|
||||
|
||||
await mx.joinRoom(room.roomId);
|
||||
await mx.joinRoom(invite.roomId);
|
||||
if (dmUserId) {
|
||||
await addRoomIdToMDirect(mx, room.roomId, dmUserId);
|
||||
await addRoomIdToMDirect(mx, invite.roomId, dmUserId);
|
||||
}
|
||||
onNavigate(room.roomId);
|
||||
}, [mx, room, userId, onNavigate])
|
||||
onNavigate(invite.roomId, invite.isSpace);
|
||||
}, [mx, invite, userId, onNavigate])
|
||||
);
|
||||
const [leaveState, leave] = useAsyncCallback<Record<string, never>, MatrixError, []>(
|
||||
useCallback(() => mx.leave(room.roomId), [mx, room])
|
||||
useCallback(() => mx.leave(invite.roomId), [mx, invite])
|
||||
);
|
||||
|
||||
const joining =
|
||||
|
|
@ -95,28 +172,43 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
|||
<SequenceCard
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="200"
|
||||
style={{ padding: config.space.S400, paddingTop: config.space.S200 }}
|
||||
gap="300"
|
||||
style={{ padding: `${config.space.S400} ${config.space.S400} ${config.space.S200}` }}
|
||||
>
|
||||
<Box gap="200" alignItems="Baseline">
|
||||
<Box grow="Yes">
|
||||
<Text size="T200" priority="300" truncate>
|
||||
Invited by <b>{senderName}</b>
|
||||
</Text>
|
||||
{(invite.isEncrypted || invite.isDirect || invite.isSpace) && (
|
||||
<Box gap="200" alignItems="Center">
|
||||
{invite.isEncrypted && (
|
||||
<Box shrink="No" alignItems="Center" justifyContent="Center">
|
||||
<Badge variant="Success" fill="Solid" size="400" radii="300">
|
||||
<Text size="L400">Encrypted</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
{invite.isDirect && (
|
||||
<Box shrink="No" alignItems="Center" justifyContent="Center">
|
||||
<Badge variant="Primary" fill="Solid" size="400" radii="300">
|
||||
<Text size="L400">Direct Message</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
{invite.isSpace && (
|
||||
<Box shrink="No" alignItems="Center" justifyContent="Center">
|
||||
<Badge variant="Secondary" fill="Soft" size="400" radii="300">
|
||||
<Text size="L400">Space</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<Time size="T200" ts={memberTs} priority="300" />
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box gap="300">
|
||||
<Avatar size="300">
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={direct ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication)}
|
||||
alt={roomName}
|
||||
roomId={invite.roomId}
|
||||
src={hideAvatar ? undefined : invite.roomAvatar}
|
||||
alt={invite.roomName}
|
||||
renderFallback={() => (
|
||||
<Text as="span" size="H6">
|
||||
{nameInitials(roomName)}
|
||||
{nameInitials(hideAvatar && invite.roomAvatar ? undefined : invite.roomName)}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
|
|
@ -125,9 +217,9 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
|||
<Box grow="Yes" direction="Column" gap="200">
|
||||
<Box direction="Column">
|
||||
<Text size="T300" truncate>
|
||||
<b>{roomName}</b>
|
||||
<b>{invite.roomName}</b>
|
||||
</Text>
|
||||
{topic && (
|
||||
{invite.roomTopic && (
|
||||
<Text
|
||||
size="T200"
|
||||
onClick={openTopic}
|
||||
|
|
@ -135,7 +227,7 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
|||
tabIndex={0}
|
||||
truncate
|
||||
>
|
||||
{topic}
|
||||
{invite.roomTopic}
|
||||
</Text>
|
||||
)}
|
||||
<Overlay open={viewTopic} backdrop={<OverlayBackdrop />}>
|
||||
|
|
@ -149,8 +241,8 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
|||
}}
|
||||
>
|
||||
<RoomTopicViewer
|
||||
name={roomName}
|
||||
topic={topic ?? ''}
|
||||
name={invite.roomName}
|
||||
topic={invite.roomTopic ?? ''}
|
||||
requestClose={closeTopic}
|
||||
/>
|
||||
</FocusTrap>
|
||||
|
|
@ -173,6 +265,7 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
|||
onClick={leave}
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
radii="300"
|
||||
fill="Soft"
|
||||
disabled={joining || leaving}
|
||||
before={leaving ? <Spinner variant="Secondary" size="100" /> : undefined}
|
||||
|
|
@ -182,28 +275,394 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
|
|||
<Button
|
||||
onClick={join}
|
||||
size="300"
|
||||
variant="Primary"
|
||||
variant="Success"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
outlined
|
||||
disabled={joining || leaving}
|
||||
before={joining ? <Spinner variant="Primary" fill="Soft" size="100" /> : undefined}
|
||||
before={joining ? <Spinner variant="Success" fill="Soft" size="100" /> : undefined}
|
||||
>
|
||||
<Text size="B300">Accept</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box gap="200" alignItems="Baseline">
|
||||
<Box grow="Yes">
|
||||
<Text size="T200" priority="300">
|
||||
From: <b>{invite.senderId}</b>
|
||||
</Text>
|
||||
</Box>
|
||||
{invite.inviteTs && (
|
||||
<Box shrink="No">
|
||||
<Time size="T200" ts={invite.inviteTs} priority="300" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</SequenceCard>
|
||||
);
|
||||
}
|
||||
|
||||
enum InviteFilter {
|
||||
Known,
|
||||
Unknown,
|
||||
Spam,
|
||||
}
|
||||
type InviteFiltersProps = {
|
||||
filter: InviteFilter;
|
||||
onFilter: (filter: InviteFilter) => void;
|
||||
knownInvites: InviteData[];
|
||||
unknownInvites: InviteData[];
|
||||
spamInvites: InviteData[];
|
||||
};
|
||||
function InviteFilters({
|
||||
filter,
|
||||
onFilter,
|
||||
knownInvites,
|
||||
unknownInvites,
|
||||
spamInvites,
|
||||
}: InviteFiltersProps) {
|
||||
const isKnown = filter === InviteFilter.Known;
|
||||
const isUnknown = filter === InviteFilter.Unknown;
|
||||
const isSpam = filter === InviteFilter.Spam;
|
||||
|
||||
return (
|
||||
<Box gap="200">
|
||||
<Chip
|
||||
variant={isKnown ? 'Success' : 'Surface'}
|
||||
aria-selected={isKnown}
|
||||
outlined={!isKnown}
|
||||
onClick={() => onFilter(InviteFilter.Known)}
|
||||
before={isKnown && <Icon size="100" src={Icons.Check} />}
|
||||
after={
|
||||
knownInvites.length > 0 && (
|
||||
<Badge variant={isKnown ? 'Success' : 'Secondary'} fill="Solid" radii="Pill">
|
||||
<Text size="L400">{knownInvites.length}</Text>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text size="T200">Primary</Text>
|
||||
</Chip>
|
||||
<Chip
|
||||
variant={isUnknown ? 'Warning' : 'Surface'}
|
||||
aria-selected={isUnknown}
|
||||
outlined={!isUnknown}
|
||||
onClick={() => onFilter(InviteFilter.Unknown)}
|
||||
before={isUnknown && <Icon size="100" src={Icons.Check} />}
|
||||
after={
|
||||
unknownInvites.length > 0 && (
|
||||
<Badge variant={isUnknown ? 'Warning' : 'Secondary'} fill="Solid" radii="Pill">
|
||||
<Text size="L400">{unknownInvites.length}</Text>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text size="T200">Public</Text>
|
||||
</Chip>
|
||||
<Chip
|
||||
variant={isSpam ? 'Critical' : 'Surface'}
|
||||
aria-selected={isSpam}
|
||||
outlined={!isSpam}
|
||||
onClick={() => onFilter(InviteFilter.Spam)}
|
||||
before={isSpam && <Icon size="100" src={Icons.Check} />}
|
||||
after={
|
||||
spamInvites.length > 0 && (
|
||||
<Badge variant={isSpam ? 'Critical' : 'Secondary'} fill="Solid" radii="Pill">
|
||||
<Text size="L400">{spamInvites.length}</Text>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text size="T200">Spam</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type KnownInvitesProps = {
|
||||
invites: InviteData[];
|
||||
handleNavigate: NavigateHandler;
|
||||
compact: boolean;
|
||||
};
|
||||
function KnownInvites({ invites, handleNavigate, compact }: KnownInvitesProps) {
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text size="H4">Primary</Text>
|
||||
{invites.length > 0 ? (
|
||||
<Box direction="Column" gap="100">
|
||||
{invites.map((invite) => (
|
||||
<InviteCard
|
||||
key={invite.roomId}
|
||||
invite={invite}
|
||||
compact={compact}
|
||||
onNavigate={handleNavigate}
|
||||
hideAvatar={false}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<PageHeroEmpty>
|
||||
<PageHeroSection>
|
||||
<PageHero
|
||||
icon={<Icon size="600" src={Icons.Mail} />}
|
||||
title="No Invites"
|
||||
subTitle="When someone you share a room with sends you an invite, it’ll show up here."
|
||||
/>
|
||||
</PageHeroSection>
|
||||
</PageHeroEmpty>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type UnknownInvitesProps = {
|
||||
invites: InviteData[];
|
||||
handleNavigate: NavigateHandler;
|
||||
compact: boolean;
|
||||
};
|
||||
function UnknownInvites({ invites, handleNavigate, compact }: UnknownInvitesProps) {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const [declineAllStatus, declineAll] = useAsyncCallback(
|
||||
useCallback(async () => {
|
||||
const roomIds = invites.map((invite) => invite.roomId);
|
||||
|
||||
await rateLimitedActions(roomIds, (roomId) => mx.leave(roomId));
|
||||
}, [mx, invites])
|
||||
);
|
||||
|
||||
const declining = declineAllStatus.status === AsyncStatus.Loading;
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Box gap="200" justifyContent="SpaceBetween" alignItems="Center">
|
||||
<Text size="H4">Public</Text>
|
||||
<Box>
|
||||
{invites.length > 0 && (
|
||||
<Chip
|
||||
variant="SurfaceVariant"
|
||||
onClick={declineAll}
|
||||
before={declining && <Spinner size="50" variant="Secondary" fill="Soft" />}
|
||||
disabled={declining}
|
||||
radii="Pill"
|
||||
>
|
||||
<Text size="T200">Decline All</Text>
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{invites.length > 0 ? (
|
||||
<Box direction="Column" gap="100">
|
||||
{invites.map((invite) => (
|
||||
<InviteCard
|
||||
key={invite.roomId}
|
||||
invite={invite}
|
||||
compact={compact}
|
||||
onNavigate={handleNavigate}
|
||||
hideAvatar
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<PageHeroEmpty>
|
||||
<PageHeroSection>
|
||||
<PageHero
|
||||
icon={<Icon size="600" src={Icons.Info} />}
|
||||
title="No Invites"
|
||||
subTitle="Invites from people outside your rooms will appear here."
|
||||
/>
|
||||
</PageHeroSection>
|
||||
</PageHeroEmpty>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type SpamInvitesProps = {
|
||||
invites: InviteData[];
|
||||
handleNavigate: NavigateHandler;
|
||||
compact: boolean;
|
||||
};
|
||||
function SpamInvites({ invites, handleNavigate, compact }: SpamInvitesProps) {
|
||||
const mx = useMatrixClient();
|
||||
const [showInvites, setShowInvites] = useState(false);
|
||||
|
||||
const reportRoomSupported = useReportRoomSupported();
|
||||
|
||||
const [declineAllStatus, declineAll] = useAsyncCallback(
|
||||
useCallback(async () => {
|
||||
const roomIds = invites.map((invite) => invite.roomId);
|
||||
|
||||
await rateLimitedActions(roomIds, (roomId) => mx.leave(roomId));
|
||||
}, [mx, invites])
|
||||
);
|
||||
|
||||
const [reportAllStatus, reportAll] = useAsyncCallback(
|
||||
useCallback(async () => {
|
||||
const roomIds = invites.map((invite) => invite.roomId);
|
||||
|
||||
await rateLimitedActions(roomIds, (roomId) => mx.reportRoom(roomId, 'Spam Invite'));
|
||||
}, [mx, invites])
|
||||
);
|
||||
|
||||
const ignoredUsers = useIgnoredUsers();
|
||||
const unignoredUsers = Array.from(new Set(invites.map((invite) => invite.senderId))).filter(
|
||||
(user) => !ignoredUsers.includes(user)
|
||||
);
|
||||
const [blockAllStatus, blockAll] = useAsyncCallback(
|
||||
useCallback(
|
||||
() => mx.setIgnoredUsers([...ignoredUsers, ...unignoredUsers]),
|
||||
[mx, ignoredUsers, unignoredUsers]
|
||||
)
|
||||
);
|
||||
|
||||
const declining = declineAllStatus.status === AsyncStatus.Loading;
|
||||
const reporting = reportAllStatus.status === AsyncStatus.Loading;
|
||||
const blocking = blockAllStatus.status === AsyncStatus.Loading;
|
||||
const loading = blocking || reporting || declining;
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text size="H4">Spam</Text>
|
||||
{invites.length > 0 ? (
|
||||
<Box direction="Column" gap="100">
|
||||
<SequenceCard
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="300"
|
||||
style={{ padding: `${config.space.S400} ${config.space.S400} 0` }}
|
||||
>
|
||||
<PageHeroSection>
|
||||
<PageHero
|
||||
icon={<Icon size="600" src={Icons.Warning} />}
|
||||
title={`${invites.length} Spam Invites`}
|
||||
subTitle="Some of the following invites may contain harmful content or have been sent by banned users."
|
||||
>
|
||||
<Box direction="Row" gap="200" justifyContent="Center" wrap="Wrap">
|
||||
<Button
|
||||
size="300"
|
||||
variant="Critical"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
onClick={declineAll}
|
||||
before={declining && <Spinner size="100" variant="Critical" fill="Solid" />}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text size="B300" truncate>
|
||||
Decline All
|
||||
</Text>
|
||||
</Button>
|
||||
{reportRoomSupported && reportAllStatus.status !== AsyncStatus.Success && (
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
onClick={reportAll}
|
||||
before={reporting && <Spinner size="100" variant="Secondary" fill="Solid" />}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text size="B300" truncate>
|
||||
Report All
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
{unignoredUsers.length > 0 && (
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
disabled={loading}
|
||||
onClick={blockAll}
|
||||
before={blocking && <Spinner size="100" variant="Secondary" fill="Solid" />}
|
||||
>
|
||||
<Text size="B300" truncate>
|
||||
Block All
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<span data-spacing-node />
|
||||
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="Pill"
|
||||
before={
|
||||
<Icon size="100" src={showInvites ? Icons.ChevronTop : Icons.ChevronBottom} />
|
||||
}
|
||||
onClick={() => setShowInvites(!showInvites)}
|
||||
>
|
||||
<Text size="B300">{showInvites ? 'Hide All' : 'View All'}</Text>
|
||||
</Button>
|
||||
</PageHero>
|
||||
</PageHeroSection>
|
||||
</SequenceCard>
|
||||
{showInvites &&
|
||||
invites.map((invite) => (
|
||||
<InviteCard
|
||||
key={invite.roomId}
|
||||
invite={invite}
|
||||
compact={compact}
|
||||
onNavigate={handleNavigate}
|
||||
hideAvatar
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<PageHeroEmpty>
|
||||
<PageHeroSection>
|
||||
<PageHero
|
||||
icon={<Icon size="600" src={Icons.Warning} />}
|
||||
title="No Spam Invites"
|
||||
subTitle="Invites detected as spam appear here."
|
||||
/>
|
||||
</PageHeroSection>
|
||||
</PageHeroEmpty>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function Invites() {
|
||||
const mx = useMatrixClient();
|
||||
const userId = mx.getUserId()!;
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const directInvites = useDirectInvites(mx, allInvitesAtom, mDirects);
|
||||
const spaceInvites = useSpaceInvites(mx, allInvitesAtom);
|
||||
const roomInvites = useRoomInvites(mx, allInvitesAtom, mDirects);
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
const allRooms = useAtomValue(allRoomsAtom);
|
||||
const allInviteIds = useAtomValue(allInvitesAtom);
|
||||
|
||||
const [filter, setFilter] = useState(InviteFilter.Known);
|
||||
|
||||
const invitesData = allInviteIds
|
||||
.map((inviteId) => mx.getRoom(inviteId))
|
||||
.filter((inviteRoom) => !!inviteRoom)
|
||||
.map((inviteRoom) => makeInviteData(mx, inviteRoom, useAuthentication));
|
||||
|
||||
const [knownInvites, unknownInvites, spamInvites] = useMemo(() => {
|
||||
const known: InviteData[] = [];
|
||||
const unknown: InviteData[] = [];
|
||||
const spam: InviteData[] = [];
|
||||
invitesData.forEach((invite) => {
|
||||
if (hasBadWords(invite) || bannedInRooms(mx, allRooms, invite.senderId)) {
|
||||
spam.push(invite);
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCommonRooms(mx, allRooms, invite.senderId).length === 0) {
|
||||
unknown.push(invite);
|
||||
return;
|
||||
}
|
||||
|
||||
known.push(invite);
|
||||
});
|
||||
|
||||
return [known, unknown, spam];
|
||||
}, [mx, allRooms, invitesData]);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [compact, setCompact] = useState(document.body.clientWidth <= COMPACT_CARD_WIDTH);
|
||||
useElementSizeObserver(
|
||||
|
|
@ -212,21 +671,12 @@ export function Invites() {
|
|||
);
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
|
||||
const renderInvite = (roomId: string, direct: boolean, handleNavigate: (rId: string) => void) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
return (
|
||||
<InviteCard
|
||||
key={roomId}
|
||||
room={room}
|
||||
userId={userId}
|
||||
compact={compact}
|
||||
direct={direct}
|
||||
onNavigate={handleNavigate}
|
||||
/>
|
||||
);
|
||||
const handleNavigate = (roomId: string, space: boolean) => {
|
||||
if (space) {
|
||||
navigateSpace(roomId);
|
||||
return;
|
||||
}
|
||||
navigateRoom(roomId);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -247,7 +697,7 @@ export function Invites() {
|
|||
<Box alignItems="Center" gap="200">
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Mail} />}
|
||||
<Text size="H3" truncate>
|
||||
Invitations
|
||||
Invites
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" basis="No" />
|
||||
|
|
@ -258,47 +708,40 @@ export function Invites() {
|
|||
<PageContent>
|
||||
<PageContentCenter>
|
||||
<Box ref={containerRef} direction="Column" gap="600">
|
||||
{directInvites.length > 0 && (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text size="H4">Direct Messages</Text>
|
||||
<Box direction="Column" gap="100">
|
||||
{directInvites.map((roomId) => renderInvite(roomId, true, navigateRoom))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box direction="Column" gap="100">
|
||||
<span data-spacing-node />
|
||||
<Text size="L400">Filter</Text>
|
||||
<InviteFilters
|
||||
filter={filter}
|
||||
onFilter={setFilter}
|
||||
knownInvites={knownInvites}
|
||||
unknownInvites={unknownInvites}
|
||||
spamInvites={spamInvites}
|
||||
/>
|
||||
</Box>
|
||||
{filter === InviteFilter.Known && (
|
||||
<KnownInvites
|
||||
invites={knownInvites}
|
||||
compact={compact}
|
||||
handleNavigate={handleNavigate}
|
||||
/>
|
||||
)}
|
||||
{spaceInvites.length > 0 && (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text size="H4">Spaces</Text>
|
||||
<Box direction="Column" gap="100">
|
||||
{spaceInvites.map((roomId) => renderInvite(roomId, false, navigateSpace))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{filter === InviteFilter.Unknown && (
|
||||
<UnknownInvites
|
||||
invites={unknownInvites}
|
||||
compact={compact}
|
||||
handleNavigate={handleNavigate}
|
||||
/>
|
||||
)}
|
||||
{roomInvites.length > 0 && (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text size="H4">Rooms</Text>
|
||||
<Box direction="Column" gap="100">
|
||||
{roomInvites.map((roomId) => renderInvite(roomId, false, navigateRoom))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{filter === InviteFilter.Spam && (
|
||||
<SpamInvites
|
||||
invites={spamInvites}
|
||||
compact={compact}
|
||||
handleNavigate={handleNavigate}
|
||||
/>
|
||||
)}
|
||||
{directInvites.length === 0 &&
|
||||
spaceInvites.length === 0 &&
|
||||
roomInvites.length === 0 && (
|
||||
<div>
|
||||
<SequenceCard
|
||||
variant="SurfaceVariant"
|
||||
style={{ padding: config.space.S400 }}
|
||||
direction="Column"
|
||||
gap="200"
|
||||
>
|
||||
<Text>No Pending Invitations</Text>
|
||||
<Text size="T200">
|
||||
You don't have any new pending invitations to display yet.
|
||||
</Text>
|
||||
</SequenceCard>
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
</PageContentCenter>
|
||||
</PageContent>
|
||||
|
|
|
|||
|
|
@ -744,13 +744,14 @@ export function SpaceTabs({ scrollRef }: SpaceTabsProps) {
|
|||
const targetSpaceId = target.getAttribute('data-id');
|
||||
if (!targetSpaceId) return;
|
||||
|
||||
const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, targetSpaceId));
|
||||
if (screenSize === ScreenSize.Mobile) {
|
||||
navigate(getSpacePath(getCanonicalAliasOrRoomId(mx, targetSpaceId)));
|
||||
navigate(spacePath);
|
||||
return;
|
||||
}
|
||||
|
||||
const activePath = navToActivePath.get(targetSpaceId);
|
||||
if (activePath) {
|
||||
if (activePath && activePath.pathname.startsWith(spacePath)) {
|
||||
navigate(joinPathComponent(activePath));
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
||||
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
|
||||
import { useSpace } from '../../../hooks/useSpace';
|
||||
import { getAllParents } from '../../../utils/room';
|
||||
import { getAllParents, getSpaceChildren } from '../../../utils/room';
|
||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
|
||||
import { mDirectAtom } from '../../../state/mDirectList';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
|
||||
export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
|
||||
const mx = useMatrixClient();
|
||||
const space = useSpace();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const [developerTools] = useSetting(settingsAtom, 'developerTools');
|
||||
const [roomToParents, setRoomToParents] = useAtom(roomToParentsAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const allRooms = useAtomValue(allRoomsAtom);
|
||||
|
||||
|
|
@ -24,12 +27,36 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
|
|||
const roomId = useSelectedRoom();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
if (
|
||||
!room ||
|
||||
room.isSpaceRoom() ||
|
||||
!allRooms.includes(room.roomId) ||
|
||||
!getAllParents(roomToParents, room.roomId).has(space.roomId)
|
||||
) {
|
||||
if (!room || !allRooms.includes(room.roomId)) {
|
||||
// room is not joined
|
||||
return (
|
||||
<JoinBeforeNavigate
|
||||
roomIdOrAlias={roomIdOrAlias!}
|
||||
eventId={eventId}
|
||||
viaServers={viaServers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (developerTools && room.isSpaceRoom() && room.roomId === space.roomId) {
|
||||
// allow to view space timeline
|
||||
return (
|
||||
<RoomProvider key={room.roomId} value={room}>
|
||||
<IsDirectRoomProvider value={mDirects.has(room.roomId)}>{children}</IsDirectRoomProvider>
|
||||
</RoomProvider>
|
||||
);
|
||||
}
|
||||
|
||||
if (!getAllParents(roomToParents, room.roomId).has(space.roomId)) {
|
||||
if (getSpaceChildren(space).includes(room.roomId)) {
|
||||
// fill missing roomToParent mapping
|
||||
setRoomToParents({
|
||||
type: 'PUT',
|
||||
parent: space.roomId,
|
||||
children: [room.roomId],
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<JoinBeforeNavigate
|
||||
roomIdOrAlias={roomIdOrAlias!}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ import {
|
|||
useRoomsNotificationPreferencesContext,
|
||||
} from '../../../hooks/useRoomsNotificationPreferences';
|
||||
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
|
||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||
|
||||
type SpaceMenuProps = {
|
||||
room: Room;
|
||||
|
|
@ -83,11 +84,13 @@ type SpaceMenuProps = {
|
|||
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClose }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [developerTools] = useSetting(settingsAtom, 'developerTools');
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
||||
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
|
||||
const openSpaceSettings = useOpenSpaceSettings();
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
|
||||
const allChild = useSpaceChildren(
|
||||
allRoomsAtom,
|
||||
|
|
@ -118,6 +121,11 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
|||
requestClose();
|
||||
};
|
||||
|
||||
const handleOpenTimeline = () => {
|
||||
navigateRoom(room.roomId);
|
||||
requestClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
|
|
@ -168,6 +176,18 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
|||
Space Settings
|
||||
</Text>
|
||||
</MenuItem>
|
||||
{developerTools && (
|
||||
<MenuItem
|
||||
onClick={handleOpenTimeline}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Terminal} />}
|
||||
radii="300"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Event Timeline
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Box>
|
||||
<Line variant="Surface" size="300" />
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
|
|
|
|||
15
src/app/plugins/bad-words.ts
Normal file
15
src/app/plugins/bad-words.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import * as badWords from 'badwords-list';
|
||||
import { sanitizeForRegex } from '../utils/regex';
|
||||
|
||||
const additionalBadWords: string[] = ['torture', 't0rture'];
|
||||
|
||||
const fullBadWordList = additionalBadWords.concat(
|
||||
badWords.array.filter((word) => !additionalBadWords.includes(word))
|
||||
);
|
||||
|
||||
export const BAD_WORDS_REGEX = new RegExp(
|
||||
`(\\b|_)(${fullBadWordList.map((word) => sanitizeForRegex(word)).join('|')})(\\b|_)`,
|
||||
'g'
|
||||
);
|
||||
|
||||
export const testBadWords = (str: string): boolean => !!str.toLowerCase().match(BAD_WORDS_REGEX);
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { atom } from 'jotai';
|
||||
import { ImportRoomKeyProgressData } from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { ImportRoomKeyProgressData, ImportRoomKeyStage } from 'matrix-js-sdk/lib/crypto-api';
|
||||
|
||||
export enum BackupProgressStatus {
|
||||
Idle,
|
||||
|
|
@ -39,22 +39,16 @@ export const backupRestoreProgressAtom = atom<
|
|||
>(
|
||||
(get) => get(baseBackupRestoreProgressAtom),
|
||||
(get, set, progress) => {
|
||||
if (progress.stage === 'fetch') {
|
||||
if (progress.stage === ImportRoomKeyStage.Fetch) {
|
||||
set(baseBackupRestoreProgressAtom, {
|
||||
status: BackupProgressStatus.Fetching,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress.stage === 'load_keys') {
|
||||
if (progress.stage === ImportRoomKeyStage.LoadKeys) {
|
||||
const { total, successes, failures } = progress;
|
||||
if (total === undefined || successes === undefined || failures === undefined) {
|
||||
// Setting to idle as https://github.com/matrix-org/matrix-js-sdk/issues/4703
|
||||
set(baseBackupRestoreProgressAtom, {
|
||||
status: BackupProgressStatus.Idle,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const downloaded = successes + failures;
|
||||
if (downloaded === total) {
|
||||
set(baseBackupRestoreProgressAtom, {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import {
|
|||
|
||||
const NAV_TO_ACTIVE_PATH = 'navToActivePath';
|
||||
|
||||
const getStoreKey = (userId: string): string => `${NAV_TO_ACTIVE_PATH}${userId}`;
|
||||
|
||||
type NavToActivePath = Map<string, Path>;
|
||||
|
||||
type NavToActivePathAction =
|
||||
|
|
@ -25,7 +27,7 @@ type NavToActivePathAction =
|
|||
export type NavToActivePathAtom = WritableAtom<NavToActivePath, [NavToActivePathAction], undefined>;
|
||||
|
||||
export const makeNavToActivePathAtom = (userId: string): NavToActivePathAtom => {
|
||||
const storeKey = `${NAV_TO_ACTIVE_PATH}${userId}`;
|
||||
const storeKey = getStoreKey(userId);
|
||||
|
||||
const baseNavToActivePathAtom = atomWithLocalStorage<NavToActivePath>(
|
||||
storeKey,
|
||||
|
|
@ -64,3 +66,7 @@ export const makeNavToActivePathAtom = (userId: string): NavToActivePathAtom =>
|
|||
|
||||
return navToActivePathAtom;
|
||||
};
|
||||
|
||||
export const clearNavToActivePathStore = (userId: string) => {
|
||||
localStorage.removeItem(getStoreKey(userId));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { encode } from 'blurhash';
|
||||
import { encode, isBlurhashValid } from 'blurhash';
|
||||
|
||||
export const encodeBlurHash = (
|
||||
img: HTMLImageElement | HTMLVideoElement,
|
||||
|
|
@ -17,3 +17,13 @@ export const encodeBlurHash = (
|
|||
const data = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
return encode(data.data, data.width, data.height, 4, 4);
|
||||
};
|
||||
|
||||
export const validBlurHash = (hash?: string): string | undefined => {
|
||||
if (typeof hash === 'string') {
|
||||
const validity = isBlurhashValid(hash);
|
||||
|
||||
return validity.result ? hash : undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -125,3 +125,9 @@ export const suffixRename = (name: string, validator: (newName: string) => boole
|
|||
};
|
||||
|
||||
export const replaceSpaceWithDash = (str: string): string => str.replace(/ /g, '-');
|
||||
|
||||
export const splitWithSpace = (content: string): string[] => {
|
||||
const trimmedContent = content.trim();
|
||||
if (trimmedContent === '') return [];
|
||||
return trimmedContent.split(' ');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,11 +13,16 @@ import {
|
|||
UploadProgress,
|
||||
UploadResponse,
|
||||
} from 'matrix-js-sdk';
|
||||
import to from 'await-to-js';
|
||||
import { IImageInfo, IThumbnailContent, IVideoInfo } from '../../types/matrix/common';
|
||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||
import { getStateEvent } from './room';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
|
||||
const DOMAIN_REGEX = /\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b/;
|
||||
|
||||
export const isServerName = (serverName: string): boolean => DOMAIN_REGEX.test(serverName);
|
||||
|
||||
export const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@!$+#])(.+):(\S+)$/);
|
||||
|
||||
export const validMxId = (id: string): boolean => !!matchMxId(id);
|
||||
|
|
@ -292,3 +297,46 @@ export const downloadEncryptedMedia = async (
|
|||
|
||||
return decryptedContent;
|
||||
};
|
||||
|
||||
export const rateLimitedActions = async <T, R = void>(
|
||||
data: T[],
|
||||
callback: (item: T, index: number) => Promise<R>,
|
||||
maxRetryCount?: number
|
||||
) => {
|
||||
let retryCount = 0;
|
||||
|
||||
let actionInterval = 0;
|
||||
|
||||
const sleepForMs = (ms: number) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
const performAction = async (dataItem: T, index: number) => {
|
||||
const [err] = await to<R, MatrixError>(callback(dataItem, index));
|
||||
|
||||
if (err?.httpStatus === 429) {
|
||||
if (retryCount === maxRetryCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const waitMS = err.getRetryAfterMs() ?? 3000;
|
||||
actionInterval = waitMS * 1.5;
|
||||
await sleepForMs(waitMS);
|
||||
retryCount += 1;
|
||||
|
||||
await performAction(dataItem, index);
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
const dataItem = data[i];
|
||||
retryCount = 0;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await performAction(dataItem, i);
|
||||
if (actionInterval > 0) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await sleepForMs(actionInterval);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
|
||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||
import {
|
||||
Membership,
|
||||
MessageEvent,
|
||||
NotificationType,
|
||||
RoomToParents,
|
||||
|
|
@ -171,7 +172,7 @@ export const getNotificationType = (mx: MatrixClient, roomId: string): Notificat
|
|||
}
|
||||
|
||||
if (!roomPushRule) {
|
||||
const overrideRules = mx.getAccountData('m.push_rules')?.getContent<IPushRules>()
|
||||
const overrideRules = mx.getAccountData(EventType.PushRules)?.getContent<IPushRules>()
|
||||
?.global?.override;
|
||||
if (!overrideRules) return NotificationType.Default;
|
||||
|
||||
|
|
@ -443,3 +444,32 @@ export const getMentionContent = (userIds: string[], room: boolean): IMentions =
|
|||
|
||||
return mMentions;
|
||||
};
|
||||
|
||||
export const getCommonRooms = (
|
||||
mx: MatrixClient,
|
||||
rooms: string[],
|
||||
otherUserId: string
|
||||
): string[] => {
|
||||
const commonRooms: string[] = [];
|
||||
|
||||
rooms.forEach((roomId) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room || room.getMyMembership() !== Membership.Join) return;
|
||||
|
||||
const common = room.hasMembershipState(otherUserId, Membership.Join);
|
||||
if (common) {
|
||||
commonRooms.push(roomId);
|
||||
}
|
||||
});
|
||||
|
||||
return commonRooms;
|
||||
};
|
||||
|
||||
export const bannedInRooms = (mx: MatrixClient, rooms: string[], otherUserId: string): boolean =>
|
||||
rooms.some((roomId) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room || room.getMyMembership() !== Membership.Join) return false;
|
||||
|
||||
const banned = room.hasMembershipState(otherUserId, Membership.Ban);
|
||||
return banned;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
|
||||
import { logger } from 'matrix-js-sdk/lib/logger';
|
||||
|
||||
import { cryptoCallbacks } from './state/secretStorageKeys';
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
logger.disableAll();
|
||||
}
|
||||
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
||||
|
||||
type Session = {
|
||||
baseUrl: string;
|
||||
|
|
@ -38,7 +34,6 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
|
|||
await indexedDBStore.startup();
|
||||
await mx.initRustCrypto();
|
||||
|
||||
mx.setGlobalErrorOnUnknownDevices(false);
|
||||
mx.setMaxListeners(50);
|
||||
|
||||
return mx;
|
||||
|
|
@ -52,6 +47,7 @@ export const startClient = async (mx: MatrixClient) => {
|
|||
|
||||
export const clearCacheAndReload = async (mx: MatrixClient) => {
|
||||
mx.stopClient();
|
||||
clearNavToActivePathStore(mx.getSafeUserId());
|
||||
await mx.store.deleteAllData();
|
||||
window.location.reload();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const cons = {
|
||||
version: '4.6.0',
|
||||
version: '4.8.1',
|
||||
secretKey: {
|
||||
ACCESS_TOKEN: 'cinny_access_token',
|
||||
DEVICE_ID: 'cinny_device_id',
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfil
|
|||
import inject from '@rollup/plugin-inject';
|
||||
import topLevelAwait from 'vite-plugin-top-level-await';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import buildConfig from './build.config';
|
||||
|
||||
const copyFiles = {
|
||||
|
|
@ -39,6 +41,32 @@ const copyFiles = {
|
|||
],
|
||||
};
|
||||
|
||||
function serverMatrixSdkCryptoWasm(wasmFilePath) {
|
||||
return {
|
||||
name: 'vite-plugin-serve-matrix-sdk-crypto-wasm',
|
||||
configureServer(server) {
|
||||
server.middlewares.use((req, res, next) => {
|
||||
if (req.url === wasmFilePath) {
|
||||
const resolvedPath = path.join(path.resolve(), "/node_modules/@matrix-org/matrix-sdk-crypto-wasm/pkg/matrix_sdk_crypto_wasm_bg.wasm");
|
||||
|
||||
if (fs.existsSync(resolvedPath)) {
|
||||
res.setHeader('Content-Type', 'application/wasm');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
|
||||
const fileStream = fs.createReadStream(resolvedPath);
|
||||
fileStream.pipe(res);
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('File not found');
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
appType: 'spa',
|
||||
publicDir: false,
|
||||
|
|
@ -46,8 +74,13 @@ export default defineConfig({
|
|||
server: {
|
||||
port: 8080,
|
||||
host: true,
|
||||
fs: {
|
||||
// Allow serving files from one level up to the project root
|
||||
allow: ['..'],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
serverMatrixSdkCryptoWasm('/node_modules/.vite/deps/pkg/matrix_sdk_crypto_wasm_bg.wasm'),
|
||||
topLevelAwait({
|
||||
// The export name of top-level await promise for each chunk module
|
||||
promiseExportName: '__tla',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue