diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml
index 441da0de..450e4e29 100644
--- a/.github/workflows/build-pull-request.yml
+++ b/.github/workflows/build-pull-request.yml
@@ -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'
diff --git a/.github/workflows/deploy-pull-request.yml b/.github/workflows/deploy-pull-request.yml
index 9c0bea78..b330c3c1 100644
--- a/.github/workflows/deploy-pull-request.yml
+++ b/.github/workflows/deploy-pull-request.yml
@@ -15,7 +15,7 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download pr number
- uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295
+ uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
with:
workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }}
@@ -24,7 +24,7 @@ jobs:
id: pr
run: echo "id=$(> $GITHUB_OUTPUT
- name: Download artifact
- uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295
+ uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
with:
workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }}
diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml
index 4e88c78d..398785ab 100644
--- a/.github/workflows/docker-pr.yml
+++ b/.github/workflows/docker-pr.yml
@@ -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
diff --git a/.github/workflows/netlify-dev.yml b/.github/workflows/netlify-dev.yml
index 34308c21..66cd5ad5 100644
--- a/.github/workflows/netlify-dev.yml
+++ b/.github/workflows/netlify-dev.yml
@@ -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'
diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml
index 44205ff2..24edda96 100644
--- a/.github/workflows/prod-deploy.yml
+++ b/.github/workflows/prod-deploy.yml
@@ -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'
@@ -52,7 +52,7 @@ jobs:
gpg --export | xxd -p
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
- name: Upload tagged release
- uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda
+ uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8
with:
files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz
@@ -72,25 +72,25 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
- name: Login to Docker Hub
- uses: docker/login-action@v3.4.0
+ uses: docker/login-action@v3.5.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to the Container registry
- uses: docker/login-action@v3.4.0
+ uses: docker/login-action@v3.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@v5.7.0
+ uses: docker/metadata-action@v5.8.0
with:
images: |
${{ 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
diff --git a/Dockerfile b/Dockerfile
index abb65ee5..718fed72 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,7 +11,7 @@ RUN npm run build
## App
-FROM nginx:1.27.4-alpine
+FROM nginx:1.29.0-alpine
COPY --from=builder /src/dist /app
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
diff --git a/package-lock.json b/package-lock.json
index f85dd74d..70826ae7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cinny",
- "version": "4.6.0",
+ "version": "4.9.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cinny",
- "version": "4.6.0",
+ "version": "4.9.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",
@@ -98,7 +99,7 @@
"prettier": "2.8.1",
"sass": "1.56.2",
"typescript": "4.9.4",
- "vite": "5.4.15",
+ "vite": "5.4.19",
"vite-plugin-pwa": "0.20.5",
"vite-plugin-static-copy": "1.0.4",
"vite-plugin-top-level-await": "1.4.4"
@@ -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",
@@ -11303,9 +11331,9 @@
}
},
"node_modules/vite": {
- "version": "5.4.15",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz",
- "integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==",
+ "version": "5.4.19",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
+ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
diff --git a/package.json b/package.json
index b489dc2c..f1816cdd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cinny",
- "version": "4.6.0",
+ "version": "4.9.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",
@@ -109,7 +110,7 @@
"prettier": "2.8.1",
"sass": "1.56.2",
"typescript": "4.9.4",
- "vite": "5.4.15",
+ "vite": "5.4.19",
"vite-plugin-pwa": "0.20.5",
"vite-plugin-static-copy": "1.0.4",
"vite-plugin-top-level-await": "1.4.4"
diff --git a/src/app/atoms/time/Time.jsx b/src/app/atoms/time/Time.jsx
index 750b958f..d7bbe439 100644
--- a/src/app/atoms/time/Time.jsx
+++ b/src/app/atoms/time/Time.jsx
@@ -4,10 +4,25 @@ import PropTypes from 'prop-types';
import dateFormat from 'dateformat';
import { isInSameDay } from '../../../util/common';
-function Time({ timestamp, fullTime }) {
+/**
+ * Renders a formatted timestamp.
+ *
+ * Displays the time in hour:minute format if the message is from today or yesterday, unless `fullTime` is true.
+ * For older messages, it shows the date and time.
+ *
+ * @param {number} timestamp - The timestamp to display.
+ * @param {boolean} [fullTime=false] - If true, always show the full date and time.
+ * @param {boolean} hour24Clock - Whether to use 24-hour time format.
+ * @param {string} dateFormatString - Format string for the date part.
+ * @returns {JSX.Element} A
} />
+ } />
join} />
} />
} />
} />
+ } />
{
document.body.className = '';
document.body.classList.add(configClass, varsClass);
document.body.classList.add(...activeTheme.classNames);
- }, [activeTheme]);
+
+ if (monochromeMode) {
+ document.body.style.filter = 'grayscale(1)';
+ } else {
+ document.body.style.filter = '';
+ }
+ }, [activeTheme, monochromeMode]);
return {children};
}
diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx
index 71891a8d..53191f33 100644
--- a/src/app/pages/auth/AuthFooter.tsx
+++ b/src/app/pages/auth/AuthFooter.tsx
@@ -15,7 +15,7 @@ export function AuthFooter() {
target="_blank"
rel="noreferrer"
>
- v4.6.0
+ v4.9.1
Twitter
diff --git a/src/app/pages/auth/SSOLogin.tsx b/src/app/pages/auth/SSOLogin.tsx
index d0cdaeb6..3ff1a229 100644
--- a/src/app/pages/auth/SSOLogin.tsx
+++ b/src/app/pages/auth/SSOLogin.tsx
@@ -1,19 +1,21 @@
import { Avatar, AvatarImage, Box, Button, Text } from 'folds';
-import { IIdentityProvider, createClient } from 'matrix-js-sdk';
+import { IIdentityProvider, SSOAction, createClient } from 'matrix-js-sdk';
import React, { useMemo } from 'react';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
type SSOLoginProps = {
providers?: IIdentityProvider[];
redirectUrl: string;
+ action?: SSOAction;
saveScreenSpace?: boolean;
};
-export function SSOLogin({ providers, redirectUrl, saveScreenSpace }: SSOLoginProps) {
+export function SSOLogin({ providers, redirectUrl, action, saveScreenSpace }: SSOLoginProps) {
const discovery = useAutoDiscoveryInfo();
const baseUrl = discovery['m.homeserver'].base_url;
const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
- const getSSOIdUrl = (ssoId?: string): string => mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId);
+ const getSSOIdUrl = (ssoId?: string): string =>
+ mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId, action);
const withoutIcon = providers
? providers.find(
diff --git a/src/app/pages/auth/login/Login.tsx b/src/app/pages/auth/login/Login.tsx
index 6b9f1223..2f04a733 100644
--- a/src/app/pages/auth/login/Login.tsx
+++ b/src/app/pages/auth/login/Login.tsx
@@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import { Box, Text, color } from 'folds';
import { Link, useSearchParams } from 'react-router-dom';
+import { SSOAction } from 'matrix-js-sdk';
import { useAuthFlows } from '../../../hooks/useAuthFlows';
import { useAuthServer } from '../../../hooks/useAuthServer';
import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows';
@@ -76,6 +77,7 @@ export function Login() {
diff --git a/src/app/pages/auth/login/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx
index 90c305d0..62f46dd2 100644
--- a/src/app/pages/auth/login/PasswordLoginForm.tsx
+++ b/src/app/pages/auth/login/PasswordLoginForm.tsx
@@ -72,19 +72,19 @@ function UsernameHint({ server }: { server: string }) {
Username:
{' '}
- johndoe
+ user123
Matrix ID:
- {` @johndoe:${server}`}
+ {` @user123:${server}`}
Email:
- {` johndoe@${server}`}
+ {` user123@${server}`}
diff --git a/src/app/pages/auth/login/loginUtil.ts b/src/app/pages/auth/login/loginUtil.ts
index 1e2248d9..7e1c7153 100644
--- a/src/app/pages/auth/login/loginUtil.ts
+++ b/src/app/pages/auth/login/loginUtil.ts
@@ -73,7 +73,7 @@ export const login = async (
}
const mx = createClient({ baseUrl: url });
- const [err, res] = await to(mx.login(data.type, data));
+ const [err, res] = await to(mx.loginRequest(data));
if (err) {
if (err.httpStatus === 400) {
diff --git a/src/app/pages/auth/register/Register.tsx b/src/app/pages/auth/register/Register.tsx
index d2986d70..7176489b 100644
--- a/src/app/pages/auth/register/Register.tsx
+++ b/src/app/pages/auth/register/Register.tsx
@@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import { Box, Text, color } from 'folds';
import { Link, useSearchParams } from 'react-router-dom';
+import { SSOAction } from 'matrix-js-sdk';
import { useAuthServer } from '../../../hooks/useAuthServer';
import { RegisterFlowStatus, useAuthFlows } from '../../../hooks/useAuthFlows';
import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows';
@@ -83,6 +84,7 @@ export function Register() {
diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx
index 846d8ff3..c48dbf53 100644
--- a/src/app/pages/client/ClientRoot.tsx
+++ b/src/app/pages/client/ClientRoot.tsx
@@ -25,7 +25,7 @@ import {
} from '../../../client/initMatrix';
import { getSecret } from '../../../client/state/auth';
import { SplashScreen } from '../../components/splash-screen';
-import { CapabilitiesAndMediaConfigLoader } from '../../components/CapabilitiesAndMediaConfigLoader';
+import { ServerConfigsLoader } from '../../components/ServerConfigsLoader';
import { CapabilitiesProvider } from '../../hooks/useCapabilities';
import { MediaConfigProvider } from '../../hooks/useMediaConfig';
import { MatrixClientProvider } from '../../hooks/useMatrixClient';
@@ -37,6 +37,7 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useSyncState } from '../../hooks/useSyncState';
import { stopPropagation } from '../../utils/keyboard';
import { SyncStatus } from './SyncStatus';
+import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
function ClientRootLoading() {
return (
@@ -207,18 +208,20 @@ export function ClientRoot({ children }: ClientRootProps) {
) : (
-
- {(capabilities, mediaConfig) => (
-
-
- {children}
-
-
-
+
+ {(serverConfigs) => (
+
+
+
+ {children}
+
+
+
+
)}
-
+
)}
diff --git a/src/app/pages/client/SidebarNav.tsx b/src/app/pages/client/SidebarNav.tsx
index 27759a2d..a2e1b682 100644
--- a/src/app/pages/client/SidebarNav.tsx
+++ b/src/app/pages/client/SidebarNav.tsx
@@ -1,14 +1,11 @@
import React, { useRef } from 'react';
-import { Icon, Icons, Scroll } from 'folds';
+import { Scroll } from 'folds';
import {
Sidebar,
SidebarContent,
SidebarStackSeparator,
SidebarStack,
- SidebarAvatar,
- SidebarItemTooltip,
- SidebarItem,
} from '../../components/sidebar';
import {
DirectTab,
@@ -18,8 +15,9 @@ import {
ExploreTab,
SettingsTab,
UnverifiedTab,
+ SearchTab,
} from './sidebar';
-import { openCreateRoom, openSearch } from '../../../client/action/navigation';
+import { CreateTab } from './sidebar/CreateTab';
export function SidebarNav() {
const scrollRef = useRef(null);
@@ -37,20 +35,7 @@ export function SidebarNav() {
-
-
- {(triggerRef) => (
- openCreateRoom(true)}
- >
-
-
- )}
-
-
+
}
@@ -58,23 +43,8 @@ export function SidebarNav() {
<>
-
-
- {(triggerRef) => (
- openSearch()}
- >
-
-
- )}
-
-
-
+
-
diff --git a/src/app/pages/client/WelcomePage.tsx b/src/app/pages/client/WelcomePage.tsx
index d2133adc..55456855 100644
--- a/src/app/pages/client/WelcomePage.tsx
+++ b/src/app/pages/client/WelcomePage.tsx
@@ -24,7 +24,7 @@ export function WelcomePage() {
target="_blank"
rel="noreferrer noopener"
>
- v4.6.0
+ v4.9.1
}
diff --git a/src/app/pages/client/create/Create.tsx b/src/app/pages/client/create/Create.tsx
new file mode 100644
index 00000000..288169b6
--- /dev/null
+++ b/src/app/pages/client/create/Create.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { Box, Icon, Icons, Scroll } from 'folds';
+import {
+ Page,
+ PageContent,
+ PageContentCenter,
+ PageHero,
+ PageHeroSection,
+} from '../../../components/page';
+import { CreateSpaceForm } from '../../../features/create-space';
+import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
+
+export function Create() {
+ const { navigateSpace } = useRoomNavigate();
+
+ return (
+
+
+
+
+
+
+
+ }
+ title="Create Space"
+ subTitle="Build a space for your community."
+ />
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/pages/client/create/index.ts b/src/app/pages/client/create/index.ts
new file mode 100644
index 00000000..48cba6e7
--- /dev/null
+++ b/src/app/pages/client/create/index.ts
@@ -0,0 +1 @@
+export * from './Create';
diff --git a/src/app/pages/client/direct/Direct.tsx b/src/app/pages/client/direct/Direct.tsx
index b6a8de1a..51de3946 100644
--- a/src/app/pages/client/direct/Direct.tsx
+++ b/src/app/pages/client/direct/Direct.tsx
@@ -17,6 +17,7 @@ import {
} from 'folds';
import { useVirtualizer } from '@tanstack/react-virtual';
import FocusTrap from 'focus-trap-react';
+import { useNavigate } from 'react-router-dom';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { factoryRoomIdByActivity } from '../../../utils/sort';
import {
@@ -28,7 +29,7 @@ import {
NavItem,
NavItemContent,
} from '../../../components/nav';
-import { getDirectRoomPath } from '../../pathUtils';
+import { getDirectCreatePath, getDirectRoomPath } from '../../pathUtils';
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
import { VirtualTile } from '../../../components/virtualizer';
@@ -38,7 +39,6 @@ import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
import { useDirectRooms } from './useDirectRooms';
-import { openInviteUser } from '../../../../client/action/navigation';
import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
import { useRoomsUnread } from '../../../state/hooks/unread';
@@ -50,6 +50,7 @@ import {
getRoomNotificationMode,
useRoomsNotificationPreferencesContext,
} from '../../../hooks/useRoomsNotificationPreferences';
+import { useDirectCreateSelected } from '../../../hooks/router/useDirectSelected';
type DirectMenuProps = {
requestClose: () => void;
@@ -138,6 +139,8 @@ function DirectHeader() {
}
function DirectEmpty() {
+ const navigate = useNavigate();
+
return (
}
options={
- openInviteUser()}>
+ navigate(getDirectCreatePath())}>
Direct Message
@@ -172,6 +175,9 @@ export function Direct() {
const directs = useDirectRooms();
const notificationPreferences = useRoomsNotificationPreferencesContext();
const roomToUnread = useAtomValue(roomToUnreadAtom);
+ const navigate = useNavigate();
+
+ const createDirectSelected = useDirectCreateSelected();
const selectedRoomId = useSelectedRoom();
const noRoomToDisplay = directs.length === 0;
@@ -205,8 +211,8 @@ export function Direct() {
-
- openInviteUser()}>
+
+ navigate(getDirectCreatePath())}>
diff --git a/src/app/pages/client/direct/DirectCreate.tsx b/src/app/pages/client/direct/DirectCreate.tsx
index 3affb9c1..3deb0b6d 100644
--- a/src/app/pages/client/direct/DirectCreate.tsx
+++ b/src/app/pages/client/direct/DirectCreate.tsx
@@ -1,33 +1,75 @@
import React, { useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
-import { WelcomePage } from '../WelcomePage';
+import { Box, Icon, IconButton, Icons, Scroll } from 'folds';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getDirectCreateSearchParams } from '../../pathSearchParam';
-import { getDirectPath, getDirectRoomPath } from '../../pathUtils';
+import { getDirectRoomPath } from '../../pathUtils';
import { getDMRoomFor } from '../../../utils/matrix';
-import { openInviteUser } from '../../../../client/action/navigation';
import { useDirectRooms } from './useDirectRooms';
+import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
+import {
+ Page,
+ PageContent,
+ PageContentCenter,
+ PageHeader,
+ PageHero,
+ PageHeroSection,
+} from '../../../components/page';
+import { BackRouteHandler } from '../../../components/BackRouteHandler';
+import { CreateChat } from '../../../features/create-chat';
export function DirectCreate() {
const mx = useMatrixClient();
+ const screenSize = useScreenSizeContext();
+
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { userId } = getDirectCreateSearchParams(searchParams);
+
const directs = useDirectRooms();
useEffect(() => {
if (userId) {
- const room = getDMRoomFor(mx, userId);
- const { roomId } = room ?? {};
+ const roomId = getDMRoomFor(mx, userId)?.roomId;
if (roomId && directs.includes(roomId)) {
navigate(getDirectRoomPath(roomId), { replace: true });
- } else {
- openInviteUser(undefined, userId);
}
- } else {
- navigate(getDirectPath(), { replace: true });
}
}, [mx, navigate, directs, userId]);
- return ;
+ return (
+
+ {screenSize === ScreenSize.Mobile && (
+
+
+
+ {(onBack) => (
+
+
+
+ )}
+
+
+
+ )}
+
+
+
+
+
+
+ }
+ title="Create Chat"
+ subTitle="Start a private, encrypted chat by entering a user ID."
+ />
+
+
+
+
+
+
+
+
+ );
}
diff --git a/src/app/pages/client/explore/Explore.tsx b/src/app/pages/client/explore/Explore.tsx
index 0c3aed2a..b7b2c7e1 100644
--- a/src/app/pages/client/explore/Explore.tsx
+++ b/src/app/pages/client/explore/Explore.tsx
@@ -342,7 +342,7 @@ export function Explore() {
key={server}
server={server}
selected={server === selectedServer}
- icon={Icons.Category}
+ icon={Icons.Server}
/>
))}
diff --git a/src/app/pages/client/explore/Server.tsx b/src/app/pages/client/explore/Server.tsx
index d0288ef4..c739e452 100644
--- a/src/app/pages/client/explore/Server.tsx
+++ b/src/app/pages/client/explore/Server.tsx
@@ -528,7 +528,7 @@ export function PublicRooms() {
)}
- {screenSize !== ScreenSize.Mobile && }
+ {screenSize !== ScreenSize.Mobile && }
{server}
diff --git a/src/app/pages/client/home/CreateRoom.tsx b/src/app/pages/client/home/CreateRoom.tsx
new file mode 100644
index 00000000..fddd75aa
--- /dev/null
+++ b/src/app/pages/client/home/CreateRoom.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { Box, Icon, Icons, Scroll, IconButton } from 'folds';
+import {
+ Page,
+ PageContent,
+ PageContentCenter,
+ PageHeader,
+ PageHero,
+ PageHeroSection,
+} from '../../../components/page';
+import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
+import { BackRouteHandler } from '../../../components/BackRouteHandler';
+import { CreateRoomForm } from '../../../features/create-room';
+import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
+
+export function HomeCreateRoom() {
+ const screenSize = useScreenSizeContext();
+
+ const { navigateRoom } = useRoomNavigate();
+
+ return (
+
+ {screenSize === ScreenSize.Mobile && (
+
+
+
+ {(onBack) => (
+
+
+
+ )}
+
+
+
+ )}
+
+
+
+
+
+
+ }
+ title="Create Room"
+ subTitle="Build a Room for Real-Time Conversations."
+ />
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/pages/client/home/Home.tsx b/src/app/pages/client/home/Home.tsx
index af4164fd..2597bb73 100644
--- a/src/app/pages/client/home/Home.tsx
+++ b/src/app/pages/client/home/Home.tsx
@@ -29,10 +29,20 @@ import {
NavItemContent,
NavLink,
} from '../../../components/nav';
-import { getExplorePath, getHomeRoomPath, getHomeSearchPath } from '../../pathUtils';
+import {
+ encodeSearchParamValueArray,
+ getExplorePath,
+ getHomeCreatePath,
+ getHomeRoomPath,
+ getHomeSearchPath,
+ withSearchParam,
+} from '../../pathUtils';
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
-import { useHomeSearchSelected } from '../../../hooks/router/useHomeSelected';
+import {
+ useHomeCreateSelected,
+ useHomeSearchSelected,
+} from '../../../hooks/router/useHomeSelected';
import { useHomeRooms } from './useHomeRooms';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { VirtualTile } from '../../../components/virtualizer';
@@ -41,7 +51,6 @@ import { makeNavCategoryId } from '../../../state/closedNavCategories';
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
-import { openCreateRoom, openJoinAlias } from '../../../../client/action/navigation';
import { PageNav, PageNavHeader, PageNavContent } from '../../../components/page';
import { useRoomsUnread } from '../../../state/hooks/unread';
import { markAsRead } from '../../../../client/action/notifications';
@@ -53,6 +62,9 @@ import {
getRoomNotificationMode,
useRoomsNotificationPreferencesContext,
} from '../../../hooks/useRoomsNotificationPreferences';
+import { UseStateProvider } from '../../../components/UseStateProvider';
+import { JoinAddressPrompt } from '../../../components/join-address-prompt';
+import { _RoomSearchParams } from '../../paths';
type HomeMenuProps = {
requestClose: () => void;
@@ -69,11 +81,6 @@ const HomeMenu = forwardRef(({ requestClose }, re
requestClose();
};
- const handleJoinAddress = () => {
- openJoinAlias();
- requestClose();
- };
-
return (
@@ -88,16 +95,6 @@ const HomeMenu = forwardRef(({ requestClose }, re
Mark as Read
- }
- >
-
- Join with Address
-
-
);
@@ -174,7 +171,7 @@ function HomeEmpty() {
}
options={
<>
- openCreateRoom()} variant="Secondary" size="300">
+ navigate(getHomeCreatePath())} variant="Secondary" size="300">
Create Room
@@ -204,8 +201,10 @@ export function Home() {
const rooms = useHomeRooms();
const notificationPreferences = useRoomsNotificationPreferencesContext();
const roomToUnread = useAtomValue(roomToUnreadAtom);
+ const navigate = useNavigate();
const selectedRoomId = useSelectedRoom();
+ const createRoomSelected = useHomeCreateSelected();
const searchSelected = useHomeSearchSelected();
const noRoomToDisplay = rooms.length === 0;
const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom());
@@ -242,8 +241,8 @@ export function Home() {
-
- openCreateRoom()}>
+
+ navigate(getHomeCreatePath())}>
@@ -258,22 +257,44 @@ export function Home() {
-
- openJoinAlias()}>
-
-
-
-
-
-
-
- Join with Address
-
-
-
-
-
-
+
+ {(open, setOpen) => (
+ <>
+
+ setOpen(true)}>
+
+
+
+
+
+
+
+ Join with Address
+
+
+
+
+
+
+ {open && (
+ setOpen(false)}
+ onOpen={(roomIdOrAlias, viaServers, eventId) => {
+ setOpen(false);
+ const path = getHomeRoomPath(roomIdOrAlias, eventId);
+ navigate(
+ viaServers
+ ? withSearchParam<_RoomSearchParams>(path, {
+ viaServers: encodeSearchParamValueArray(viaServers),
+ })
+ : path
+ );
+ }}
+ />
+ )}
+ >
+ )}
+
diff --git a/src/app/pages/client/inbox/Inbox.tsx b/src/app/pages/client/inbox/Inbox.tsx
index 686296b7..67d6021a 100644
--- a/src/app/pages/client/inbox/Inbox.tsx
+++ b/src/app/pages/client/inbox/Inbox.tsx
@@ -32,7 +32,7 @@ function InvitesNavItem() {
- Invitations
+ Invites
{inviteCount > 0 && }
diff --git a/src/app/pages/client/inbox/Invites.tsx b/src/app/pages/client/inbox/Invites.tsx
index 8dcfa1c2..20518e56 100644
--- a/src/app/pages/client/inbox/Invites.tsx
+++ b/src/app/pages/client/inbox/Invites.tsx
@@ -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,149 @@ 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';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
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;
+ reason?: string;
+
+ 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()?.topic ??
+ undefined;
+
const member = room.getMember(userId);
const memberEvent = member?.events.member;
- const memberTs = memberEvent?.getTs() ?? 0;
+
+ const content = memberEvent?.getContent();
const senderId = memberEvent?.getSender();
+
const senderName = senderId
? getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId
: undefined;
+ const inviteTs = memberEvent?.getTs();
+ const reason =
+ content && 'reason' in content && typeof content.reason === 'string'
+ ? content.reason
+ : undefined;
- const topic = useRoomTopic(room);
+ return {
+ room,
+ roomId: room.roomId,
+ roomAvatar,
+ roomName,
+ roomTopic,
+ roomAlias: room.getCanonicalAlias() ?? undefined,
+
+ senderId: senderId ?? 'Unknown',
+ senderName: senderName ?? 'Unknown',
+ inviteTs,
+ reason,
+
+ 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) ||
+ testBadWords(invite.reason || '');
+
+type NavigateHandler = (roomId: string, space: boolean) => void;
+
+type InviteCardProps = {
+ invite: InviteData;
+ compact?: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
+ onNavigate: NavigateHandler;
+ hideAvatar: boolean;
+};
+function InviteCard({
+ invite,
+ compact,
+ hour24Clock,
+ dateFormatString,
+ onNavigate,
+ hideAvatar,
+}: InviteCardProps) {
+ const mx = useMatrixClient();
+ const userId = mx.getSafeUserId();
const [viewTopic, setViewTopic] = useState(false);
const closeTopic = () => setViewTopic(false);
@@ -73,17 +168,19 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
const [joinState, join] = useAsyncCallback(
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, MatrixError, []>(
- useCallback(() => mx.leave(room.roomId), [mx, room])
+ useCallback(() => mx.leave(invite.roomId), [mx, invite])
);
const joining =
@@ -95,28 +192,43 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
-
-
-
- Invited by {senderName}
-
+ {(invite.isEncrypted || invite.isDirect || invite.isSpace) && (
+
+ {invite.isEncrypted && (
+
+
+ Encrypted
+
+
+ )}
+ {invite.isDirect && (
+
+
+ Direct Message
+
+
+ )}
+ {invite.isSpace && (
+
+
+ Space
+
+
+ )}
-
-
-
-
+ )}
(
- {nameInitials(roomName)}
+ {nameInitials(hideAvatar && invite.roomAvatar ? undefined : invite.roomName)}
)}
/>
@@ -125,9 +237,9 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
- {roomName}
+ {invite.roomName}
- {topic && (
+ {invite.roomTopic && (
- {topic}
+ {invite.roomTopic}
)}
}>
@@ -149,8 +261,8 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
}}
>
@@ -173,6 +285,7 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
onClick={leave}
size="300"
variant="Secondary"
+ radii="300"
fill="Soft"
disabled={joining || leaving}
before={leaving ? : undefined}
@@ -182,28 +295,437 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
: undefined}
+ before={joining ? : undefined}
>
Accept
+
+
+
+
+ From: {invite.senderId}
+
+
+ {typeof invite.inviteTs === 'number' && invite.inviteTs !== 0 && (
+
+
+
+ )}
+
+ {invite.reason && (
+
+ Reason: {invite.reason}
+
+ )}
+
);
}
+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 (
+
+ onFilter(InviteFilter.Known)}
+ before={isKnown && }
+ after={
+ knownInvites.length > 0 && (
+
+ {knownInvites.length}
+
+ )
+ }
+ >
+ Primary
+
+ onFilter(InviteFilter.Unknown)}
+ before={isUnknown && }
+ after={
+ unknownInvites.length > 0 && (
+
+ {unknownInvites.length}
+
+ )
+ }
+ >
+ Public
+
+ onFilter(InviteFilter.Spam)}
+ before={isSpam && }
+ after={
+ spamInvites.length > 0 && (
+
+ {spamInvites.length}
+
+ )
+ }
+ >
+ Spam
+
+
+ );
+}
+
+type KnownInvitesProps = {
+ invites: InviteData[];
+ handleNavigate: NavigateHandler;
+ compact: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
+};
+function KnownInvites({
+ invites,
+ handleNavigate,
+ compact,
+ hour24Clock,
+ dateFormatString,
+}: KnownInvitesProps) {
+ return (
+
+ Primary
+ {invites.length > 0 ? (
+
+ {invites.map((invite) => (
+
+ ))}
+
+ ) : (
+
+
+ }
+ title="No Invites"
+ subTitle="When someone you share a room with sends you an invite, it’ll show up here."
+ />
+
+
+ )}
+
+ );
+}
+
+type UnknownInvitesProps = {
+ invites: InviteData[];
+ handleNavigate: NavigateHandler;
+ compact: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
+};
+function UnknownInvites({
+ invites,
+ handleNavigate,
+ compact,
+ hour24Clock,
+ dateFormatString,
+}: 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 (
+
+
+ Public
+
+ {invites.length > 0 && (
+ }
+ disabled={declining}
+ radii="Pill"
+ >
+ Decline All
+
+ )}
+
+
+ {invites.length > 0 ? (
+
+ {invites.map((invite) => (
+
+ ))}
+
+ ) : (
+
+
+ }
+ title="No Invites"
+ subTitle="Invites from people outside your rooms will appear here."
+ />
+
+
+ )}
+
+ );
+}
+
+type SpamInvitesProps = {
+ invites: InviteData[];
+ handleNavigate: NavigateHandler;
+ compact: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
+};
+function SpamInvites({
+ invites,
+ handleNavigate,
+ compact,
+ hour24Clock,
+ dateFormatString,
+}: 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 (
+
+ Spam
+ {invites.length > 0 ? (
+
+
+
+ }
+ title={`${invites.length} Spam Invites`}
+ subTitle="Some of the following invites may contain harmful content or have been sent by banned users."
+ >
+
+ }
+ disabled={loading}
+ >
+
+ Decline All
+
+
+ {reportRoomSupported && reportAllStatus.status !== AsyncStatus.Success && (
+ }
+ disabled={loading}
+ >
+
+ Report All
+
+
+ )}
+ {unignoredUsers.length > 0 && (
+ }
+ >
+
+ Block All
+
+
+ )}
+
+
+
+
+
+ }
+ onClick={() => setShowInvites(!showInvites)}
+ >
+ {showInvites ? 'Hide All' : 'View All'}
+
+
+
+
+ {showInvites &&
+ invites.map((invite) => (
+
+ ))}
+
+ ) : (
+
+
+ }
+ title="No Spam Invites"
+ subTitle="Invites detected as spam appear here."
+ />
+
+
+ )}
+
+ );
+}
+
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(null);
const [compact, setCompact] = useState(document.body.clientWidth <= COMPACT_CARD_WIDTH);
useElementSizeObserver(
@@ -212,21 +734,15 @@ export function Invites() {
);
const screenSize = useScreenSizeContext();
- const { navigateRoom, navigateSpace } = useRoomNavigate();
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
- const renderInvite = (roomId: string, direct: boolean, handleNavigate: (rId: string) => void) => {
- const room = mx.getRoom(roomId);
- if (!room) return null;
- return (
-
- );
+ const handleNavigate = (roomId: string, space: boolean) => {
+ if (space) {
+ navigateSpace(roomId);
+ return;
+ }
+ navigateRoom(roomId);
};
return (
@@ -247,7 +763,7 @@ export function Invites() {
{screenSize !== ScreenSize.Mobile && }
- Invitations
+ Invites
@@ -258,47 +774,46 @@ export function Invites() {
- {directInvites.length > 0 && (
-
- Direct Messages
-
- {directInvites.map((roomId) => renderInvite(roomId, true, navigateRoom))}
-
-
+
+
+ Filter
+
+
+ {filter === InviteFilter.Known && (
+
)}
- {spaceInvites.length > 0 && (
-
- Spaces
-
- {spaceInvites.map((roomId) => renderInvite(roomId, false, navigateSpace))}
-
-
+
+ {filter === InviteFilter.Unknown && (
+
)}
- {roomInvites.length > 0 && (
-
- Rooms
-
- {roomInvites.map((roomId) => renderInvite(roomId, false, navigateRoom))}
-
-
+
+ {filter === InviteFilter.Spam && (
+
)}
- {directInvites.length === 0 &&
- spaceInvites.length === 0 &&
- roomInvites.length === 0 && (
-
-
- No Pending Invitations
-
- You don't have any new pending invitations to display yet.
-
-
-
- )}
diff --git a/src/app/pages/client/inbox/Notifications.tsx b/src/app/pages/client/inbox/Notifications.tsx
index 80ce25a9..afdfec6d 100644
--- a/src/app/pages/client/inbox/Notifications.tsx
+++ b/src/app/pages/client/inbox/Notifications.tsx
@@ -84,16 +84,19 @@ import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { allRoomsAtom } from '../../../state/room-list/roomList';
-import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
-import {
- getTagIconSrc,
- useAccessibleTagColors,
- usePowerLevelTags,
-} from '../../../hooks/usePowerLevelTags';
+import { usePowerLevels } from '../../../hooks/usePowerLevels';
+import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
import { useTheme } from '../../../hooks/useTheme';
import { PowerIcon } from '../../../components/power';
import colorMXID from '../../../../util/colorMXID';
import { mDirectAtom } from '../../../state/mDirectList';
+import {
+ getPowerTagIconSrc,
+ useAccessiblePowerTagColors,
+ useGetMemberPowerTag,
+} from '../../../hooks/useMemberPowerTag';
+import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
type RoomNotificationsGroup = {
roomId: string;
@@ -205,6 +208,8 @@ type RoomNotificationsGroupProps = {
hideActivity: boolean;
onOpen: (roomId: string, eventId: string) => void;
legacyUsernameColor?: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
};
function RoomNotificationsGroupComp({
room,
@@ -214,16 +219,22 @@ function RoomNotificationsGroupComp({
hideActivity,
onOpen,
legacyUsernameColor,
+ hour24Clock,
+ dateFormatString,
}: RoomNotificationsGroupProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevels(room);
- const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
- const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
+ const creators = useRoomCreators(room);
+
+ const creatorsTag = useRoomCreatorsTag();
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
+ const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+
const theme = useTheme();
- const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
+ const accessibleTagColors = useAccessiblePowerTagColors(theme.kind, creatorsTag, powerLevelTags);
const mentionClickHandler = useMentionClickHandler(room.roomId);
const spoilerClickHandler = useSpoilerClickHandler();
@@ -443,13 +454,12 @@ function RoomNotificationsGroupComp({
const threadRootId =
relation?.rel_type === RelationType.Thread ? relation.event_id : undefined;
- const senderPowerLevel = getPowerLevel(event.sender);
- const powerLevelTag = getPowerLevelTag(senderPowerLevel);
- const tagColor = powerLevelTag?.color
- ? accessibleTagColors?.get(powerLevelTag.color)
+ const memberPowerTag = getMemberPowerTag(event.sender);
+ const tagColor = memberPowerTag?.color
+ ? accessibleTagColors?.get(memberPowerTag.color)
: undefined;
- const tagIconSrc = powerLevelTag?.icon
- ? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
+ const tagIconSrc = memberPowerTag?.icon
+ ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
: undefined;
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
@@ -496,7 +506,11 @@ function RoomNotificationsGroupComp({
{tagIconSrc && }
-
+
@@ -549,6 +562,8 @@ export function Notifications() {
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const screenSize = useScreenSizeContext();
const mDirects = useAtomValue(mDirectAtom);
@@ -713,6 +728,8 @@ export function Notifications() {
legacyUsernameColor={
legacyUsernameColor || mDirects.has(groupRoom.roomId)
}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
/>
);
diff --git a/src/app/pages/client/sidebar/CreateTab.tsx b/src/app/pages/client/sidebar/CreateTab.tsx
new file mode 100644
index 00000000..e6575cb4
--- /dev/null
+++ b/src/app/pages/client/sidebar/CreateTab.tsx
@@ -0,0 +1,134 @@
+import React, { MouseEventHandler, useState } from 'react';
+import { Box, config, Icon, Icons, Menu, PopOut, RectCords, Text } from 'folds';
+import FocusTrap from 'focus-trap-react';
+import { useNavigate } from 'react-router-dom';
+import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '../../../components/sidebar';
+import { stopPropagation } from '../../../utils/keyboard';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SettingTile } from '../../../components/setting-tile';
+import { ContainerColor } from '../../../styles/ContainerColor.css';
+import {
+ encodeSearchParamValueArray,
+ getCreatePath,
+ getSpacePath,
+ withSearchParam,
+} from '../../pathUtils';
+import { useCreateSelected } from '../../../hooks/router/useCreateSelected';
+import { JoinAddressPrompt } from '../../../components/join-address-prompt';
+import { _RoomSearchParams } from '../../paths';
+
+export function CreateTab() {
+ const createSelected = useCreateSelected();
+
+ const navigate = useNavigate();
+ const [menuCords, setMenuCords] = useState();
+ const [joinAddress, setJoinAddress] = useState(false);
+
+ const handleMenu: MouseEventHandler = (evt) => {
+ setMenuCords(menuCords ? undefined : evt.currentTarget.getBoundingClientRect());
+ };
+
+ const handleCreateSpace = () => {
+ navigate(getCreatePath());
+ setMenuCords(undefined);
+ };
+
+ const handleJoinWithAddress = () => {
+ setJoinAddress(true);
+ setMenuCords(undefined);
+ };
+
+ return (
+
+
+ {(triggerRef) => (
+ setMenuCords(undefined),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) =>
+ evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+ isKeyBackward: (evt: KeyboardEvent) =>
+ evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+
+
+
+ }>
+ Create Space
+
+ Build a space for your community.
+
+
+
+
+ }>
+ Join with Address
+
+ Become a part of existing community.
+
+
+
+
+
+
+ }
+ >
+
+
+
+ {joinAddress && (
+ setJoinAddress(false)}
+ onOpen={(roomIdOrAlias, viaServers) => {
+ setJoinAddress(false);
+ const path = getSpacePath(roomIdOrAlias);
+ navigate(
+ viaServers
+ ? withSearchParam<_RoomSearchParams>(path, {
+ viaServers: encodeSearchParamValueArray(viaServers),
+ })
+ : path
+ );
+ }}
+ />
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/src/app/pages/client/sidebar/SearchTab.tsx b/src/app/pages/client/sidebar/SearchTab.tsx
new file mode 100644
index 00000000..7ceb5c49
--- /dev/null
+++ b/src/app/pages/client/sidebar/SearchTab.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { Icon, Icons } from 'folds';
+import { useAtom } from 'jotai';
+import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '../../../components/sidebar';
+import { searchModalAtom } from '../../../state/searchModal';
+
+export function SearchTab() {
+ const [opened, setOpen] = useAtom(searchModalAtom);
+
+ const open = () => setOpen(true);
+
+ return (
+
+
+ {(triggerRef) => (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/app/pages/client/sidebar/SettingsTab.tsx b/src/app/pages/client/sidebar/SettingsTab.tsx
index 83cd118c..bb212184 100644
--- a/src/app/pages/client/sidebar/SettingsTab.tsx
+++ b/src/app/pages/client/sidebar/SettingsTab.tsx
@@ -28,7 +28,7 @@ export function SettingsTab() {
return (
-
+
{(triggerRef) => (
(
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room);
- const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
- const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
+ const creators = useRoomCreators(room);
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+ const canInvite = permissions.action('invite', mx.getSafeUserId());
const openSpaceSettings = useOpenSpaceSettings();
+ const [invitePrompt, setInvitePrompt] = useState(false);
+
const allChild = useSpaceChildren(
allRoomsAtom,
room.roomId,
@@ -132,8 +138,7 @@ const SpaceMenu = forwardRef(
};
const handleInvite = () => {
- openInviteUser(room.roomId);
- requestClose();
+ setInvitePrompt(true);
};
const handleRoomSettings = () => {
@@ -143,6 +148,15 @@ const SpaceMenu = forwardRef(
return (
+ {invitePrompt && room && (
+ {
+ setInvitePrompt(false);
+ requestClose();
+ }}
+ />
+ )}
+ )}
@@ -264,6 +305,75 @@ function SpaceHeader() {
);
}
+type SpaceTombstoneProps = { roomId: string; replacementRoomId: string };
+export function SpaceTombstone({ roomId, replacementRoomId }: SpaceTombstoneProps) {
+ const mx = useMatrixClient();
+ const { navigateSpace } = useRoomNavigate();
+
+ const [joinState, handleJoin] = useAsyncCallback(
+ useCallback(() => {
+ const currentRoom = mx.getRoom(roomId);
+ const via = currentRoom ? getViaServers(currentRoom) : [];
+ return mx.joinRoom(replacementRoomId, {
+ viaServers: via,
+ });
+ }, [mx, roomId, replacementRoomId])
+ );
+ const replacementRoom = mx.getRoom(replacementRoomId);
+
+ const handleOpen = () => {
+ if (replacementRoom) navigateSpace(replacementRoom.roomId);
+ if (joinState.status === AsyncStatus.Success) navigateSpace(joinState.data.roomId);
+ };
+
+ return (
+
+
+ Space Upgraded
+ This space has been replaced and is no longer active.
+ {joinState.status === AsyncStatus.Error && (
+
+ {(joinState.error as any)?.message ?? 'Failed to join replacement space!'}
+
+ )}
+
+
+ {replacementRoom?.getMyMembership() === Membership.Join ||
+ joinState.status === AsyncStatus.Success ? (
+
+ Open New Space
+
+ ) : (
+
+ )
+ }
+ disabled={joinState.status === AsyncStatus.Loading}
+ >
+ Join New Space
+
+ )}
+
+
+ );
+}
+
export function Space() {
const mx = useMatrixClient();
const space = useSpace();
@@ -276,6 +386,8 @@ export function Space() {
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
const notificationPreferences = useRoomsNotificationPreferencesContext();
+ const tombstoneEvent = useStateEvent(space, StateEvent.RoomTombstone);
+
const selectedRoomId = useSelectedRoom();
const lobbySelected = useSpaceLobbySelected(spaceIdOrAlias);
const searchSelected = useSpaceSearchSelected(spaceIdOrAlias);
@@ -331,6 +443,12 @@ export function Space() {
+ {tombstoneEvent && (
+
+ )}
diff --git a/src/app/pages/pathUtils.ts b/src/app/pages/pathUtils.ts
index cbd453ae..817d21d1 100644
--- a/src/app/pages/pathUtils.ts
+++ b/src/app/pages/pathUtils.ts
@@ -22,6 +22,7 @@ import {
SPACE_PATH,
SPACE_ROOM_PATH,
SPACE_SEARCH_PATH,
+ CREATE_PATH,
} from './paths';
import { trimLeadingSlash, trimTrailingSlash } from '../utils/common';
import { HashRouterConfig } from '../hooks/useClientConfig';
@@ -152,6 +153,8 @@ export const getExploreServerPath = (server: string): string => {
return generatePath(EXPLORE_SERVER_PATH, params);
};
+export const getCreatePath = (): string => CREATE_PATH;
+
export const getInboxPath = (): string => INBOX_PATH;
export const getInboxNotificationsPath = (): string => INBOX_NOTIFICATIONS_PATH;
export const getInboxInvitesPath = (): string => INBOX_INVITES_PATH;
diff --git a/src/app/pages/paths.ts b/src/app/pages/paths.ts
index 54da8922..9cf4bfb9 100644
--- a/src/app/pages/paths.ts
+++ b/src/app/pages/paths.ts
@@ -74,6 +74,8 @@ export type ExploreServerPathSearchParams = {
};
export const EXPLORE_SERVER_PATH = `/explore/${_SERVER_PATH}`;
+export const CREATE_PATH = '/create';
+
export const _NOTIFICATIONS_PATH = 'notifications/';
export const _INVITES_PATH = 'invites/';
export const INBOX_PATH = '/inbox/';
diff --git a/src/app/plugins/bad-words.ts b/src/app/plugins/bad-words.ts
new file mode 100644
index 00000000..bb6073d8
--- /dev/null
+++ b/src/app/plugins/bad-words.ts
@@ -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);
diff --git a/src/app/plugins/matrix-to.ts b/src/app/plugins/matrix-to.ts
index c9df0a87..feeafe05 100644
--- a/src/app/plugins/matrix-to.ts
+++ b/src/app/plugins/matrix-to.ts
@@ -42,9 +42,9 @@ const MATRIX_TO = /^https?:\/\/matrix\.to\S*$/;
export const testMatrixTo = (href: string): boolean => MATRIX_TO.test(href);
const MATRIX_TO_USER = /^https?:\/\/matrix\.to\/#\/(@[^:\s]+:[^?/\s]+)\/?$/;
-const MATRIX_TO_ROOM = /^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/?(\?[\S]*)?$/;
+const MATRIX_TO_ROOM = /^https?:\/\/matrix\.to\/#\/([#!][^?/\s]+)\/?(\?[\S]*)?$/;
const MATRIX_TO_ROOM_EVENT =
- /^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/(\$[^?/\s]+)\/?(\?[\S]*)?$/;
+ /^https?:\/\/matrix\.to\/#\/([#!][^?/\s]+)\/(\$[^?/\s]+)\/?(\?[\S]*)?$/;
export const parseMatrixToUser = (href: string): string | undefined => {
const match = href.match(MATRIX_TO_USER);
diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx
index cd683e36..ba40c978 100644
--- a/src/app/plugins/react-custom-html-parser.tsx
+++ b/src/app/plugins/react-custom-html-parser.tsx
@@ -1,5 +1,12 @@
/* eslint-disable jsx-a11y/alt-text */
-import React, { ComponentPropsWithoutRef, ReactEventHandler, Suspense, lazy } from 'react';
+import React, {
+ ComponentPropsWithoutRef,
+ ReactEventHandler,
+ Suspense,
+ lazy,
+ useMemo,
+ useState,
+} from 'react';
import {
Element,
Text as DOMText,
@@ -9,10 +16,11 @@ import {
} from 'html-react-parser';
import { MatrixClient } from 'matrix-js-sdk';
import classNames from 'classnames';
-import { Scroll, Text } from 'folds';
+import { Box, Chip, config, Header, Icon, IconButton, Icons, Scroll, Text, toRem } from 'folds';
import { IntermediateRepresentation, Opts as LinkifyOpts, OptFn } from 'linkifyjs';
import Linkify from 'linkify-react';
import { ErrorBoundary } from 'react-error-boundary';
+import { ChildNode } from 'domhandler';
import * as css from '../styles/CustomHtml.css';
import {
getMxIdLocalPart,
@@ -31,7 +39,8 @@ import {
testMatrixTo,
} from './matrix-to';
import { onEnterOrSpace } from '../utils/keyboard';
-import { tryDecodeURIComponent } from '../utils/dom';
+import { copyToClipboard, tryDecodeURIComponent } from '../utils/dom';
+import { useTimeoutToggle } from '../hooks/useTimeoutToggle';
const ReactPrism = lazy(() => import('./react-prism/ReactPrism'));
@@ -195,6 +204,111 @@ export const highlightText = (
);
});
+/**
+ * Recursively extracts and concatenates all text content from an array of ChildNode objects.
+ *
+ * @param {ChildNode[]} nodes - An array of ChildNode objects to extract text from.
+ * @returns {string} The concatenated plain text content of all descendant text nodes.
+ */
+const extractTextFromChildren = (nodes: ChildNode[]): string => {
+ let text = '';
+
+ nodes.forEach((node) => {
+ if (node.type === 'text') {
+ text += node.data;
+ } else if (node instanceof Element && node.children) {
+ text += extractTextFromChildren(node.children);
+ }
+ });
+
+ return text;
+};
+
+export function CodeBlock({
+ children,
+ opts,
+}: {
+ children: ChildNode[];
+ opts: HTMLReactParserOptions;
+}) {
+ const code = children[0];
+ const languageClass =
+ code instanceof Element && code.name === 'code' ? code.attribs.class : undefined;
+ const language =
+ languageClass && languageClass.startsWith('language-')
+ ? languageClass.replace('language-', '')
+ : languageClass;
+
+ const LINE_LIMIT = 14;
+ const largeCodeBlock = useMemo(
+ () => extractTextFromChildren(children).split('\n').length > LINE_LIMIT,
+ [children]
+ );
+
+ const [expanded, setExpand] = useState(false);
+ const [copied, setCopied] = useTimeoutToggle();
+
+ const handleCopy = () => {
+ copyToClipboard(extractTextFromChildren(children));
+ setCopied();
+ };
+
+ const toggleExpand = () => {
+ setExpand(!expanded);
+ };
+
+ return (
+
+
+
+
+ {language ?? 'Code'}
+
+
+
+ }
+ >
+ {copied ? 'Copied' : 'Copy'}
+
+ {largeCodeBlock && (
+
+
+
+ )}
+
+
+
+
+ {domToReact(children, opts)}
+
+
+ {largeCodeBlock && !expanded && }
+
+ );
+}
+
export const getReactCustomHtmlParser = (
mx: MatrixClient,
roomId: string | undefined,
@@ -269,19 +383,7 @@ export const getReactCustomHtmlParser = (
}
if (name === 'pre') {
- return (
-
-
- {domToReact(children, opts)}
-
-
- );
+ return {children};
}
if (name === 'blockquote') {
@@ -331,9 +433,9 @@ export const getReactCustomHtmlParser = (
}
} else {
return (
-
+
{domToReact(children, opts)}
-
+
);
}
}
diff --git a/src/app/plugins/react-prism/ReactPrism.tsx b/src/app/plugins/react-prism/ReactPrism.tsx
index f93c6ef1..ab2e9320 100644
--- a/src/app/plugins/react-prism/ReactPrism.tsx
+++ b/src/app/plugins/react-prism/ReactPrism.tsx
@@ -2,18 +2,307 @@ import React, { MutableRefObject, ReactNode, useEffect, useRef } from 'react';
import Prism from 'prismjs';
-import 'prismjs/components/prism-json';
-import 'prismjs/components/prism-javascript';
-import 'prismjs/components/prism-typescript';
-import 'prismjs/components/prism-css';
-import 'prismjs/components/prism-sass';
-import 'prismjs/components/prism-swift';
-import 'prismjs/components/prism-rust';
-import 'prismjs/components/prism-go';
-import 'prismjs/components/prism-c';
-import 'prismjs/components/prism-cpp';
-import 'prismjs/components/prism-java';
-import 'prismjs/components/prism-python';
+import 'prismjs/components/prism-abap.js';
+import 'prismjs/components/prism-abnf.js';
+import 'prismjs/components/prism-actionscript.js';
+import 'prismjs/components/prism-ada.js';
+import 'prismjs/components/prism-agda.js';
+import 'prismjs/components/prism-al.js';
+import 'prismjs/components/prism-antlr4.js';
+import 'prismjs/components/prism-apacheconf.js';
+import 'prismjs/components/prism-apex.js';
+import 'prismjs/components/prism-apl.js';
+import 'prismjs/components/prism-applescript.js';
+import 'prismjs/components/prism-aql.js';
+import 'prismjs/components/prism-arff.js';
+import 'prismjs/components/prism-armasm.js';
+import 'prismjs/components/prism-arturo.js';
+import 'prismjs/components/prism-asciidoc.js';
+import 'prismjs/components/prism-asm6502.js';
+import 'prismjs/components/prism-asmatmel.js';
+import 'prismjs/components/prism-aspnet.js';
+import 'prismjs/components/prism-autohotkey.js';
+import 'prismjs/components/prism-autoit.js';
+import 'prismjs/components/prism-avisynth.js';
+import 'prismjs/components/prism-avro-idl.js';
+import 'prismjs/components/prism-awk.js';
+import 'prismjs/components/prism-bash.js';
+import 'prismjs/components/prism-basic.js';
+import 'prismjs/components/prism-batch.js';
+import 'prismjs/components/prism-bbcode.js';
+import 'prismjs/components/prism-bbj.js';
+import 'prismjs/components/prism-bicep.js';
+import 'prismjs/components/prism-birb.js';
+import 'prismjs/components/prism-bnf.js';
+import 'prismjs/components/prism-bqn.js';
+import 'prismjs/components/prism-brainfuck.js';
+import 'prismjs/components/prism-brightscript.js';
+import 'prismjs/components/prism-bro.js';
+import 'prismjs/components/prism-bsl.js';
+import 'prismjs/components/prism-c.js';
+import 'prismjs/components/prism-cfscript.js';
+import 'prismjs/components/prism-cil.js';
+import 'prismjs/components/prism-cilkc.js';
+import 'prismjs/components/prism-cilkcpp.js';
+import 'prismjs/components/prism-clike.js';
+import 'prismjs/components/prism-clojure.js';
+import 'prismjs/components/prism-cmake.js';
+import 'prismjs/components/prism-cobol.js';
+import 'prismjs/components/prism-coffeescript.js';
+import 'prismjs/components/prism-concurnas.js';
+import 'prismjs/components/prism-cooklang.js';
+import 'prismjs/components/prism-coq.js';
+import 'prismjs/components/prism-cpp.js';
+import 'prismjs/components/prism-csharp.js';
+import 'prismjs/components/prism-cshtml.js';
+import 'prismjs/components/prism-csp.js';
+import 'prismjs/components/prism-css-extras.js';
+import 'prismjs/components/prism-css.js';
+import 'prismjs/components/prism-csv.js';
+import 'prismjs/components/prism-cue.js';
+import 'prismjs/components/prism-cypher.js';
+import 'prismjs/components/prism-d.js';
+import 'prismjs/components/prism-dart.js';
+import 'prismjs/components/prism-dataweave.js';
+import 'prismjs/components/prism-dax.js';
+import 'prismjs/components/prism-dhall.js';
+import 'prismjs/components/prism-diff.js';
+import 'prismjs/components/prism-dns-zone-file.js';
+import 'prismjs/components/prism-docker.js';
+import 'prismjs/components/prism-dot.js';
+import 'prismjs/components/prism-ebnf.js';
+import 'prismjs/components/prism-editorconfig.js';
+import 'prismjs/components/prism-eiffel.js';
+import 'prismjs/components/prism-ejs.js';
+import 'prismjs/components/prism-elixir.js';
+import 'prismjs/components/prism-elm.js';
+import 'prismjs/components/prism-erb.js';
+import 'prismjs/components/prism-erlang.js';
+import 'prismjs/components/prism-etlua.js';
+import 'prismjs/components/prism-excel-formula.js';
+import 'prismjs/components/prism-factor.js';
+import 'prismjs/components/prism-false.js';
+import 'prismjs/components/prism-firestore-security-rules.js';
+import 'prismjs/components/prism-flow.js';
+import 'prismjs/components/prism-fortran.js';
+import 'prismjs/components/prism-fsharp.js';
+import 'prismjs/components/prism-ftl.js';
+import 'prismjs/components/prism-gap.js';
+import 'prismjs/components/prism-gcode.js';
+import 'prismjs/components/prism-gdscript.js';
+import 'prismjs/components/prism-gedcom.js';
+import 'prismjs/components/prism-gettext.js';
+import 'prismjs/components/prism-gherkin.js';
+import 'prismjs/components/prism-git.js';
+import 'prismjs/components/prism-glsl.js';
+import 'prismjs/components/prism-gml.js';
+import 'prismjs/components/prism-gn.js';
+import 'prismjs/components/prism-go-module.js';
+import 'prismjs/components/prism-go.js';
+import 'prismjs/components/prism-gradle.js';
+import 'prismjs/components/prism-graphql.js';
+import 'prismjs/components/prism-groovy.js';
+import 'prismjs/components/prism-haml.js';
+import 'prismjs/components/prism-handlebars.js';
+import 'prismjs/components/prism-haskell.js';
+import 'prismjs/components/prism-haxe.js';
+import 'prismjs/components/prism-hcl.js';
+import 'prismjs/components/prism-hlsl.js';
+import 'prismjs/components/prism-hoon.js';
+import 'prismjs/components/prism-hpkp.js';
+import 'prismjs/components/prism-hsts.js';
+import 'prismjs/components/prism-http.js';
+import 'prismjs/components/prism-ichigojam.js';
+import 'prismjs/components/prism-icon.js';
+import 'prismjs/components/prism-icu-message-format.js';
+import 'prismjs/components/prism-idris.js';
+import 'prismjs/components/prism-iecst.js';
+import 'prismjs/components/prism-ignore.js';
+import 'prismjs/components/prism-inform7.js';
+import 'prismjs/components/prism-ini.js';
+import 'prismjs/components/prism-io.js';
+import 'prismjs/components/prism-j.js';
+import 'prismjs/components/prism-java.js';
+import 'prismjs/components/prism-javadoclike.js';
+import 'prismjs/components/prism-javascript.js';
+import 'prismjs/components/prism-javastacktrace.js';
+import 'prismjs/components/prism-jexl.js';
+import 'prismjs/components/prism-jolie.js';
+import 'prismjs/components/prism-jq.js';
+import 'prismjs/components/prism-js-extras.js';
+import 'prismjs/components/prism-js-templates.js';
+import 'prismjs/components/prism-json.js';
+import 'prismjs/components/prism-json5.js';
+import 'prismjs/components/prism-jsonp.js';
+import 'prismjs/components/prism-jsstacktrace.js';
+import 'prismjs/components/prism-jsx.js';
+import 'prismjs/components/prism-julia.js';
+import 'prismjs/components/prism-keepalived.js';
+import 'prismjs/components/prism-keyman.js';
+import 'prismjs/components/prism-kotlin.js';
+import 'prismjs/components/prism-kumir.js';
+import 'prismjs/components/prism-kusto.js';
+import 'prismjs/components/prism-latex.js';
+import 'prismjs/components/prism-latte.js';
+import 'prismjs/components/prism-less.js';
+import 'prismjs/components/prism-lilypond.js';
+import 'prismjs/components/prism-linker-script.js';
+import 'prismjs/components/prism-liquid.js';
+import 'prismjs/components/prism-lisp.js';
+import 'prismjs/components/prism-livescript.js';
+import 'prismjs/components/prism-llvm.js';
+import 'prismjs/components/prism-log.js';
+import 'prismjs/components/prism-lolcode.js';
+import 'prismjs/components/prism-lua.js';
+import 'prismjs/components/prism-magma.js';
+import 'prismjs/components/prism-makefile.js';
+import 'prismjs/components/prism-markdown.js';
+import 'prismjs/components/prism-markup-templating.js';
+import 'prismjs/components/prism-markup.js';
+import 'prismjs/components/prism-mata.js';
+import 'prismjs/components/prism-matlab.js';
+import 'prismjs/components/prism-maxscript.js';
+import 'prismjs/components/prism-mel.js';
+import 'prismjs/components/prism-mermaid.js';
+import 'prismjs/components/prism-metafont.js';
+import 'prismjs/components/prism-mizar.js';
+import 'prismjs/components/prism-mongodb.js';
+import 'prismjs/components/prism-monkey.js';
+import 'prismjs/components/prism-moonscript.js';
+import 'prismjs/components/prism-n1ql.js';
+import 'prismjs/components/prism-n4js.js';
+import 'prismjs/components/prism-nand2tetris-hdl.js';
+import 'prismjs/components/prism-naniscript.js';
+import 'prismjs/components/prism-nasm.js';
+import 'prismjs/components/prism-neon.js';
+import 'prismjs/components/prism-nevod.js';
+import 'prismjs/components/prism-nginx.js';
+import 'prismjs/components/prism-nim.js';
+import 'prismjs/components/prism-nix.js';
+import 'prismjs/components/prism-nsis.js';
+import 'prismjs/components/prism-objectivec.js';
+import 'prismjs/components/prism-ocaml.js';
+import 'prismjs/components/prism-odin.js';
+import 'prismjs/components/prism-opencl.js';
+import 'prismjs/components/prism-openqasm.js';
+import 'prismjs/components/prism-oz.js';
+import 'prismjs/components/prism-parigp.js';
+import 'prismjs/components/prism-parser.js';
+import 'prismjs/components/prism-pascal.js';
+import 'prismjs/components/prism-pascaligo.js';
+import 'prismjs/components/prism-pcaxis.js';
+import 'prismjs/components/prism-peoplecode.js';
+import 'prismjs/components/prism-perl.js';
+import 'prismjs/components/prism-php-extras.js';
+import 'prismjs/components/prism-php.js';
+import 'prismjs/components/prism-phpdoc.js';
+import 'prismjs/components/prism-plant-uml.js';
+import 'prismjs/components/prism-powerquery.js';
+import 'prismjs/components/prism-powershell.js';
+import 'prismjs/components/prism-processing.js';
+import 'prismjs/components/prism-prolog.js';
+import 'prismjs/components/prism-promql.js';
+import 'prismjs/components/prism-properties.js';
+import 'prismjs/components/prism-protobuf.js';
+import 'prismjs/components/prism-psl.js';
+import 'prismjs/components/prism-pug.js';
+import 'prismjs/components/prism-puppet.js';
+import 'prismjs/components/prism-pure.js';
+import 'prismjs/components/prism-purebasic.js';
+import 'prismjs/components/prism-purescript.js';
+import 'prismjs/components/prism-python.js';
+import 'prismjs/components/prism-q.js';
+import 'prismjs/components/prism-qml.js';
+import 'prismjs/components/prism-qore.js';
+import 'prismjs/components/prism-qsharp.js';
+import 'prismjs/components/prism-r.js';
+import 'prismjs/components/prism-reason.js';
+import 'prismjs/components/prism-regex.js';
+import 'prismjs/components/prism-rego.js';
+import 'prismjs/components/prism-renpy.js';
+import 'prismjs/components/prism-rescript.js';
+import 'prismjs/components/prism-rest.js';
+import 'prismjs/components/prism-rip.js';
+import 'prismjs/components/prism-roboconf.js';
+import 'prismjs/components/prism-robotframework.js';
+import 'prismjs/components/prism-ruby.js';
+import 'prismjs/components/prism-rust.js';
+import 'prismjs/components/prism-sas.js';
+import 'prismjs/components/prism-sass.js';
+import 'prismjs/components/prism-scala.js';
+import 'prismjs/components/prism-scheme.js';
+import 'prismjs/components/prism-scss.js';
+import 'prismjs/components/prism-shell-session.js';
+import 'prismjs/components/prism-smali.js';
+import 'prismjs/components/prism-smalltalk.js';
+import 'prismjs/components/prism-smarty.js';
+import 'prismjs/components/prism-sml.js';
+import 'prismjs/components/prism-solidity.js';
+import 'prismjs/components/prism-solution-file.js';
+import 'prismjs/components/prism-soy.js';
+import 'prismjs/components/prism-splunk-spl.js';
+import 'prismjs/components/prism-sqf.js';
+import 'prismjs/components/prism-sql.js';
+import 'prismjs/components/prism-squirrel.js';
+import 'prismjs/components/prism-stan.js';
+import 'prismjs/components/prism-stata.js';
+import 'prismjs/components/prism-stylus.js';
+import 'prismjs/components/prism-supercollider.js';
+import 'prismjs/components/prism-swift.js';
+import 'prismjs/components/prism-systemd.js';
+import 'prismjs/components/prism-t4-templating.js';
+import 'prismjs/components/prism-t4-vb.js';
+import 'prismjs/components/prism-tap.js';
+import 'prismjs/components/prism-tcl.js';
+import 'prismjs/components/prism-textile.js';
+import 'prismjs/components/prism-toml.js';
+import 'prismjs/components/prism-tremor.js';
+import 'prismjs/components/prism-tsx.js';
+import 'prismjs/components/prism-tt2.js';
+import 'prismjs/components/prism-turtle.js';
+import 'prismjs/components/prism-twig.js';
+import 'prismjs/components/prism-typescript.js';
+import 'prismjs/components/prism-typoscript.js';
+import 'prismjs/components/prism-unrealscript.js';
+import 'prismjs/components/prism-uorazor.js';
+import 'prismjs/components/prism-uri.js';
+import 'prismjs/components/prism-v.js';
+import 'prismjs/components/prism-vala.js';
+import 'prismjs/components/prism-vbnet.js';
+import 'prismjs/components/prism-velocity.js';
+import 'prismjs/components/prism-verilog.js';
+import 'prismjs/components/prism-vhdl.js';
+import 'prismjs/components/prism-vim.js';
+import 'prismjs/components/prism-visual-basic.js';
+import 'prismjs/components/prism-warpscript.js';
+import 'prismjs/components/prism-wasm.js';
+import 'prismjs/components/prism-web-idl.js';
+import 'prismjs/components/prism-wgsl.js';
+import 'prismjs/components/prism-wiki.js';
+import 'prismjs/components/prism-wolfram.js';
+import 'prismjs/components/prism-wren.js';
+import 'prismjs/components/prism-xeora.js';
+import 'prismjs/components/prism-xml-doc.js';
+import 'prismjs/components/prism-xojo.js';
+import 'prismjs/components/prism-xquery.js';
+import 'prismjs/components/prism-yaml.js';
+import 'prismjs/components/prism-yang.js';
+import 'prismjs/components/prism-zig.js';
+import 'prismjs/components/prism-arduino.js';
+
+// Broken:
+//
+// import 'prismjs/components/prism-bison.js';
+// import 'prismjs/components/prism-chaiscript.js';
+// import 'prismjs/components/prism-core.js';
+// import 'prismjs/components/prism-crystal.js';
+// import 'prismjs/components/prism-django.js';
+// import 'prismjs/components/prism-javadoc.js';
+// import 'prismjs/components/prism-jsdoc.js';
+// import 'prismjs/components/prism-plsql.js';
+// import 'prismjs/components/prism-racket.js';
+// import 'prismjs/components/prism-sparql.js';
+// import 'prismjs/components/prism-t4-cs.js';
import './ReactPrism.css';
// using classNames .prism-dark .prism-light from ReactPrism.css
diff --git a/src/app/plugins/via-servers.ts b/src/app/plugins/via-servers.ts
index 75470999..d825a1fd 100644
--- a/src/app/plugins/via-servers.ts
+++ b/src/app/plugins/via-servers.ts
@@ -1,11 +1,19 @@
import { Room } from 'matrix-js-sdk';
import { IPowerLevels } from '../hooks/usePowerLevels';
-import { getMxIdServer } from '../utils/matrix';
-import { StateEvent } from '../../types/matrix/room';
+import { creatorsSupported, getMxIdServer } from '../utils/matrix';
+import { IRoomCreateContent, StateEvent } from '../../types/matrix/room';
import { getStateEvent } from '../utils/room';
export const getViaServers = (room: Room): string[] => {
const getHighestPowerUserId = (): string | undefined => {
+ const creatorEvent = getStateEvent(room, StateEvent.RoomCreate);
+ if (
+ creatorEvent &&
+ creatorsSupported(creatorEvent.getContent().room_version)
+ ) {
+ return creatorEvent.getSender();
+ }
+
const powerLevels = getStateEvent(room, StateEvent.RoomPowerLevels)?.getContent();
if (!powerLevels) return undefined;
diff --git a/src/app/state/backupRestore.ts b/src/app/state/backupRestore.ts
index 2f86b4d5..ad14e5d7 100644
--- a/src/app/state/backupRestore.ts
+++ b/src/app/state/backupRestore.ts
@@ -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, {
diff --git a/src/app/state/createRoomModal.ts b/src/app/state/createRoomModal.ts
new file mode 100644
index 00000000..81af5d5b
--- /dev/null
+++ b/src/app/state/createRoomModal.ts
@@ -0,0 +1,7 @@
+import { atom } from 'jotai';
+
+export type CreateRoomModalState = {
+ spaceId?: string;
+};
+
+export const createRoomModalAtom = atom(undefined);
diff --git a/src/app/state/createSpaceModal.ts b/src/app/state/createSpaceModal.ts
new file mode 100644
index 00000000..fe4db755
--- /dev/null
+++ b/src/app/state/createSpaceModal.ts
@@ -0,0 +1,7 @@
+import { atom } from 'jotai';
+
+export type CreateSpaceModalState = {
+ spaceId?: string;
+};
+
+export const createSpaceModalAtom = atom(undefined);
diff --git a/src/app/state/hooks/createRoomModal.ts b/src/app/state/hooks/createRoomModal.ts
new file mode 100644
index 00000000..15db7289
--- /dev/null
+++ b/src/app/state/hooks/createRoomModal.ts
@@ -0,0 +1,34 @@
+import { useCallback } from 'react';
+import { useAtomValue, useSetAtom } from 'jotai';
+import { createRoomModalAtom, CreateRoomModalState } from '../createRoomModal';
+
+export const useCreateRoomModalState = (): CreateRoomModalState | undefined => {
+ const data = useAtomValue(createRoomModalAtom);
+
+ return data;
+};
+
+type CloseCallback = () => void;
+export const useCloseCreateRoomModal = (): CloseCallback => {
+ const setSettings = useSetAtom(createRoomModalAtom);
+
+ const close: CloseCallback = useCallback(() => {
+ setSettings(undefined);
+ }, [setSettings]);
+
+ return close;
+};
+
+type OpenCallback = (space?: string) => void;
+export const useOpenCreateRoomModal = (): OpenCallback => {
+ const setSettings = useSetAtom(createRoomModalAtom);
+
+ const open: OpenCallback = useCallback(
+ (spaceId) => {
+ setSettings({ spaceId });
+ },
+ [setSettings]
+ );
+
+ return open;
+};
diff --git a/src/app/state/hooks/createSpaceModal.ts b/src/app/state/hooks/createSpaceModal.ts
new file mode 100644
index 00000000..ea7cb47a
--- /dev/null
+++ b/src/app/state/hooks/createSpaceModal.ts
@@ -0,0 +1,34 @@
+import { useCallback } from 'react';
+import { useAtomValue, useSetAtom } from 'jotai';
+import { createSpaceModalAtom, CreateSpaceModalState } from '../createSpaceModal';
+
+export const useCreateSpaceModalState = (): CreateSpaceModalState | undefined => {
+ const data = useAtomValue(createSpaceModalAtom);
+
+ return data;
+};
+
+type CloseCallback = () => void;
+export const useCloseCreateSpaceModal = (): CloseCallback => {
+ const setSettings = useSetAtom(createSpaceModalAtom);
+
+ const close: CloseCallback = useCallback(() => {
+ setSettings(undefined);
+ }, [setSettings]);
+
+ return close;
+};
+
+type OpenCallback = (space?: string) => void;
+export const useOpenCreateSpaceModal = (): OpenCallback => {
+ const setSettings = useSetAtom(createSpaceModalAtom);
+
+ const open: OpenCallback = useCallback(
+ (spaceId) => {
+ setSettings({ spaceId });
+ },
+ [setSettings]
+ );
+
+ return open;
+};
diff --git a/src/app/state/hooks/userRoomProfile.ts b/src/app/state/hooks/userRoomProfile.ts
new file mode 100644
index 00000000..7fed0c08
--- /dev/null
+++ b/src/app/state/hooks/userRoomProfile.ts
@@ -0,0 +1,41 @@
+import { useCallback } from 'react';
+import { useAtomValue, useSetAtom } from 'jotai';
+import { Position, RectCords } from 'folds';
+import { userRoomProfileAtom, UserRoomProfileState } from '../userRoomProfile';
+
+export const useUserRoomProfileState = (): UserRoomProfileState | undefined => {
+ const data = useAtomValue(userRoomProfileAtom);
+
+ return data;
+};
+
+type CloseCallback = () => void;
+export const useCloseUserRoomProfile = (): CloseCallback => {
+ const setUserRoomProfile = useSetAtom(userRoomProfileAtom);
+
+ const close: CloseCallback = useCallback(() => {
+ setUserRoomProfile(undefined);
+ }, [setUserRoomProfile]);
+
+ return close;
+};
+
+type OpenCallback = (
+ roomId: string,
+ spaceId: string | undefined,
+ userId: string,
+ cords: RectCords,
+ position?: Position
+) => void;
+export const useOpenUserRoomProfile = (): OpenCallback => {
+ const setUserRoomProfile = useSetAtom(userRoomProfileAtom);
+
+ const open: OpenCallback = useCallback(
+ (roomId, spaceId, userId, cords, position) => {
+ setUserRoomProfile({ roomId, spaceId, userId, cords, position });
+ },
+ [setUserRoomProfile]
+ );
+
+ return open;
+};
diff --git a/src/app/state/navToActivePath.ts b/src/app/state/navToActivePath.ts
index 80869146..af90c914 100644
--- a/src/app/state/navToActivePath.ts
+++ b/src/app/state/navToActivePath.ts
@@ -9,6 +9,8 @@ import {
const NAV_TO_ACTIVE_PATH = 'navToActivePath';
+const getStoreKey = (userId: string): string => `${NAV_TO_ACTIVE_PATH}${userId}`;
+
type NavToActivePath = Map;
type NavToActivePathAction =
@@ -25,7 +27,7 @@ type NavToActivePathAction =
export type NavToActivePathAtom = WritableAtom;
export const makeNavToActivePathAtom = (userId: string): NavToActivePathAtom => {
- const storeKey = `${NAV_TO_ACTIVE_PATH}${userId}`;
+ const storeKey = getStoreKey(userId);
const baseNavToActivePathAtom = atomWithLocalStorage(
storeKey,
@@ -64,3 +66,7 @@ export const makeNavToActivePathAtom = (userId: string): NavToActivePathAtom =>
return navToActivePathAtom;
};
+
+export const clearNavToActivePathStore = (userId: string) => {
+ localStorage.removeItem(getStoreKey(userId));
+};
diff --git a/src/app/state/searchModal.ts b/src/app/state/searchModal.ts
new file mode 100644
index 00000000..3893e5e3
--- /dev/null
+++ b/src/app/state/searchModal.ts
@@ -0,0 +1,3 @@
+import { atom } from 'jotai';
+
+export const searchModalAtom = atom(false);
diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts
index 799747ac..31ee6ccb 100644
--- a/src/app/state/settings.ts
+++ b/src/app/state/settings.ts
@@ -1,6 +1,7 @@
import { atom } from 'jotai';
const STORAGE_KEY = 'settings';
+export type DateFormat = 'D MMM YYYY' | 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD' | '';
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
export enum MessageLayout {
Modern = 0,
@@ -13,6 +14,7 @@ export interface Settings {
useSystemTheme: boolean;
lightThemeId?: string;
darkThemeId?: string;
+ monochromeMode?: boolean;
isMarkdown: boolean;
editorToolbar: boolean;
twitterEmoji: boolean;
@@ -35,6 +37,9 @@ export interface Settings {
showNotifications: boolean;
isNotificationSounds: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
+
developerTools: boolean;
}
@@ -43,6 +48,7 @@ const defaultSettings: Settings = {
useSystemTheme: true,
lightThemeId: undefined,
darkThemeId: undefined,
+ monochromeMode: false,
isMarkdown: true,
editorToolbar: false,
twitterEmoji: false,
@@ -65,6 +71,9 @@ const defaultSettings: Settings = {
showNotifications: true,
isNotificationSounds: true,
+ hour24Clock: false,
+ dateFormatString: 'D MMM YYYY',
+
developerTools: false,
};
diff --git a/src/app/state/userRoomProfile.ts b/src/app/state/userRoomProfile.ts
new file mode 100644
index 00000000..cf4e403a
--- /dev/null
+++ b/src/app/state/userRoomProfile.ts
@@ -0,0 +1,12 @@
+import { Position, RectCords } from 'folds';
+import { atom } from 'jotai';
+
+export type UserRoomProfileState = {
+ userId: string;
+ roomId: string;
+ spaceId?: string;
+ cords: RectCords;
+ position?: Position;
+};
+
+export const userRoomProfileAtom = atom(undefined);
diff --git a/src/app/styles/ContainerColor.css.ts b/src/app/styles/ContainerColor.css.ts
index cb1f933d..cefc5256 100644
--- a/src/app/styles/ContainerColor.css.ts
+++ b/src/app/styles/ContainerColor.css.ts
@@ -1,6 +1,6 @@
import { ComplexStyleRule } from '@vanilla-extract/css';
import { RecipeVariants, recipe } from '@vanilla-extract/recipes';
-import { ContainerColor as TContainerColor, DefaultReset, color } from 'folds';
+import { ContainerColor as TContainerColor, DefaultReset, color, config } from 'folds';
const getVariant = (variant: TContainerColor): ComplexStyleRule => ({
vars: {
@@ -9,6 +9,20 @@ const getVariant = (variant: TContainerColor): ComplexStyleRule => ({
outlineColor: color[variant].ContainerLine,
color: color[variant].OnContainer,
},
+ selectors: {
+ 'button&[aria-pressed=true]': {
+ backgroundColor: color[variant].ContainerActive,
+ },
+ 'button&:hover, &:focus-visible': {
+ backgroundColor: color[variant].ContainerHover,
+ },
+ 'button&:active': {
+ backgroundColor: color[variant].ContainerActive,
+ },
+ 'button&[disabled]': {
+ opacity: config.opacity.Disabled,
+ },
+ },
});
export const ContainerColor = recipe({
diff --git a/src/app/styles/CustomHtml.css.ts b/src/app/styles/CustomHtml.css.ts
index d86a3236..f717669c 100644
--- a/src/app/styles/CustomHtml.css.ts
+++ b/src/app/styles/CustomHtml.css.ts
@@ -41,16 +41,19 @@ export const BlockQuote = style([
]);
const BaseCode = style({
- fontFamily: 'monospace',
- color: color.Secondary.OnContainer,
- background: color.Secondary.Container,
- border: `${config.borderWidth.B300} solid ${color.Secondary.ContainerLine}`,
+ color: color.SurfaceVariant.OnContainer,
+ background: color.SurfaceVariant.Container,
+ border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
borderRadius: config.radii.R300,
});
+const CodeFont = style({
+ fontFamily: 'monospace',
+});
export const Code = style([
DefaultReset,
BaseCode,
+ CodeFont,
{
padding: `0 ${config.space.S100}`,
},
@@ -85,10 +88,32 @@ export const CodeBlock = style([
MarginSpaced,
{
fontStyle: 'normal',
+ position: 'relative',
+ overflow: 'hidden',
},
]);
-export const CodeBlockInternal = style({
- padding: `${config.space.S200} ${config.space.S200} 0`,
+export const CodeBlockHeader = style({
+ padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
+ borderBottomWidth: config.borderWidth.B300,
+ gap: config.space.S200,
+});
+export const CodeBlockInternal = style([
+ CodeFont,
+ {
+ padding: `${config.space.S200} ${config.space.S200} 0`,
+ minWidth: toRem(200),
+ },
+]);
+
+export const CodeBlockBottomShadow = style({
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ pointerEvents: 'none',
+
+ height: config.space.S400,
+ background: `linear-gradient(to top, #00000022, #00000000)`,
});
export const List = style([
diff --git a/src/app/utils/blurHash.ts b/src/app/utils/blurHash.ts
index 3fe1ade0..566f6d18 100644
--- a/src/app/utils/blurHash.ts
+++ b/src/app/utils/blurHash.ts
@@ -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;
+};
diff --git a/src/app/utils/common.ts b/src/app/utils/common.ts
index d230c6bb..678f1b6e 100644
--- a/src/app/utils/common.ts
+++ b/src/app/utils/common.ts
@@ -18,6 +18,13 @@ export const millisecondsToMinutesAndSeconds = (milliseconds: number): string =>
return `${mm}:${ss < 10 ? '0' : ''}${ss}`;
};
+export const millisecondsToMinutes = (milliseconds: number): string => {
+ const seconds = Math.floor(milliseconds / 1000);
+ const mm = Math.floor(seconds / 60);
+
+ return mm.toString();
+};
+
export const secondsToMinutesAndSeconds = (seconds: number): string => {
const mm = Math.floor(seconds / 60);
const ss = Math.round(seconds % 60);
@@ -125,3 +132,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(' ');
+};
diff --git a/src/app/utils/dom.ts b/src/app/utils/dom.ts
index f4c3f719..80db3ae7 100644
--- a/src/app/utils/dom.ts
+++ b/src/app/utils/dom.ts
@@ -43,6 +43,17 @@ export const canFitInScrollView = (
export type FilesOrFile = T extends true ? File[] : File;
+export const getFilesFromFileList = (fileList: FileList): File[] => {
+ const files: File[] = [];
+
+ for (let i = 0; i < fileList.length; i += 1) {
+ const file: File | undefined = fileList[i];
+ if (file instanceof File) files.push(file);
+ }
+
+ return files;
+};
+
export const selectFile = (
accept: string,
multiple?: M
@@ -58,7 +69,7 @@ export const selectFile = (
if (!fileList) {
resolve(undefined);
} else {
- const files: File[] = [...fileList].filter((file) => file);
+ const files: File[] = getFilesFromFileList(fileList);
resolve((multiple ? files : files[0]) as FilesOrFile);
}
input.removeEventListener('change', changeHandler);
@@ -70,7 +81,7 @@ export const selectFile = (
export const getDataTransferFiles = (dataTransfer: DataTransfer): File[] | undefined => {
const fileList = dataTransfer.files;
- const files = [...fileList].filter((file) => file);
+ const files: File[] = getFilesFromFileList(fileList);
if (files.length === 0) return undefined;
return files;
};
@@ -224,3 +235,10 @@ export const notificationPermission = (permission: NotificationPermission) => {
}
return false;
};
+
+export const getMouseEventCords = (event: MouseEvent) => ({
+ x: event.clientX,
+ y: event.clientY,
+ width: 0,
+ height: 0,
+});
diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts
index cd3c0862..4c86c4e2 100644
--- a/src/app/utils/matrix.ts
+++ b/src/app/utils/matrix.ts
@@ -13,14 +13,19 @@ 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';
+import { Membership, StateEvent } from '../../types/matrix/room';
-export const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@!$+#])(.+):(\S+)$/);
+const DOMAIN_REGEX = /\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b/;
-export const validMxId = (id: string): boolean => !!matchMxId(id);
+export const isServerName = (serverName: string): boolean => DOMAIN_REGEX.test(serverName);
+
+const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@$+#])([^\s:]+):(\S+)$/);
+
+const validMxId = (id: string): boolean => !!matchMxId(id);
export const getMxIdServer = (userId: string): string | undefined => matchMxId(userId)?.[3];
@@ -28,7 +33,7 @@ export const getMxIdLocalPart = (userId: string): string | undefined => matchMxI
export const isUserId = (id: string): boolean => validMxId(id) && id.startsWith('@');
-export const isRoomId = (id: string): boolean => validMxId(id) && id.startsWith('!');
+export const isRoomId = (id: string): boolean => id.startsWith('!');
export const isRoomAlias = (id: string): boolean => validMxId(id) && id.startsWith('#');
@@ -45,7 +50,11 @@ export const getCanonicalAliasOrRoomId = (mx: MatrixClient, roomId: string): str
const room = mx.getRoom(roomId);
if (!room) return roomId;
if (getStateEvent(room, StateEvent.RoomTombstone) !== undefined) return roomId;
- return room.getCanonicalAlias() || roomId;
+ const alias = room.getCanonicalAlias();
+ if (alias && getCanonicalAliasRoomId(mx, alias) === roomId) {
+ return alias;
+ }
+ return roomId;
};
export const getImageInfo = (img: HTMLImageElement, fileOrBlob: File | Blob): IImageInfo => {
@@ -173,7 +182,12 @@ export const eventWithShortcode = (ev: MatrixEvent) =>
export const getDMRoomFor = (mx: MatrixClient, userId: string): Room | undefined => {
const dmLikeRooms = mx
.getRooms()
- .filter((room) => room.hasEncryptionStateEvent() && room.getMembers().length <= 2);
+ .filter(
+ (room) =>
+ room.getMyMembership() === Membership.Join &&
+ room.hasEncryptionStateEvent() &&
+ room.getMembers().length <= 2
+ );
return dmLikeRooms.find((room) => room.getMember(userId));
};
@@ -216,8 +230,11 @@ export const addRoomIdToMDirect = async (
roomId: string,
userId: string
): Promise => {
- const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct);
- const userIdToRoomIds: Record = mDirectsEvent?.getContent() ?? {};
+ const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as any);
+ let userIdToRoomIds: Record = {};
+
+ if (typeof mDirectsEvent !== 'undefined')
+ userIdToRoomIds = structuredClone(mDirectsEvent.getContent());
// remove it from the lists of any others users
// (it can only be a DM room for one person)
@@ -238,12 +255,15 @@ export const addRoomIdToMDirect = async (
}
userIdToRoomIds[userId] = roomIds;
- await mx.setAccountData(AccountDataEvent.Direct, userIdToRoomIds);
+ await mx.setAccountData(AccountDataEvent.Direct as any, userIdToRoomIds as any);
};
export const removeRoomIdFromMDirect = async (mx: MatrixClient, roomId: string): Promise => {
- const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct);
- const userIdToRoomIds: Record = mDirectsEvent?.getContent() ?? {};
+ const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as any);
+ let userIdToRoomIds: Record = {};
+
+ if (typeof mDirectsEvent !== 'undefined')
+ userIdToRoomIds = structuredClone(mDirectsEvent.getContent());
Object.keys(userIdToRoomIds).forEach((targetUserId) => {
const roomIds = userIdToRoomIds[targetUserId];
@@ -253,7 +273,7 @@ export const removeRoomIdFromMDirect = async (mx: MatrixClient, roomId: string):
}
});
- await mx.setAccountData(AccountDataEvent.Direct, userIdToRoomIds);
+ await mx.setAccountData(AccountDataEvent.Direct as any, userIdToRoomIds as any);
};
export const mxcUrlToHttp = (
@@ -292,3 +312,63 @@ export const downloadEncryptedMedia = async (
return decryptedContent;
};
+
+export const rateLimitedActions = async (
+ data: T[],
+ callback: (item: T, index: number) => Promise,
+ 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(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);
+ }
+ }
+};
+
+export const knockSupported = (version: string): boolean => {
+ const unsupportedVersion = ['1', '2', '3', '4', '5', '6'];
+ return !unsupportedVersion.includes(version);
+};
+export const restrictedSupported = (version: string): boolean => {
+ const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7'];
+ return !unsupportedVersion.includes(version);
+};
+export const knockRestrictedSupported = (version: string): boolean => {
+ const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
+ return !unsupportedVersion.includes(version);
+};
+export const creatorsSupported = (version: string): boolean => {
+ const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
+ return !unsupportedVersion.includes(version);
+};
diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts
index 3bf8cd5a..b4bba2ad 100644
--- a/src/app/utils/room.ts
+++ b/src/app/utils/room.ts
@@ -5,6 +5,7 @@ import {
EventTimelineSet,
EventType,
IMentions,
+ IPowerLevelsContent,
IPushRule,
IPushRules,
JoinRule,
@@ -19,6 +20,8 @@ import {
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
import { AccountDataEvent } from '../../types/matrix/accountData';
import {
+ IRoomCreateContent,
+ Membership,
MessageEvent,
NotificationType,
RoomToParents,
@@ -41,7 +44,7 @@ export const getStateEvents = (room: Room, eventType: StateEvent): MatrixEvent[]
export const getAccountData = (
mx: MatrixClient,
eventType: AccountDataEvent
-): MatrixEvent | undefined => mx.getAccountData(eventType);
+): MatrixEvent | undefined => mx.getAccountData(eventType as any);
export const getMDirects = (mDirectEvent: MatrixEvent): Set => {
const roomIds = new Set();
@@ -171,7 +174,7 @@ export const getNotificationType = (mx: MatrixClient, roomId: string): Notificat
}
if (!roomPushRule) {
- const overrideRules = mx.getAccountData('m.push_rules')?.getContent()
+ const overrideRules = mx.getAccountData(EventType.PushRules)?.getContent()
?.global?.override;
if (!overrideRules) return NotificationType.Default;
@@ -292,9 +295,14 @@ export const getDirectRoomAvatarUrl = (
useAuthentication = false
): string | undefined => {
const mxcUrl = room.getAvatarFallbackMember()?.getMxcAvatarUrl();
- return mxcUrl
- ? mx.mxcUrlToHttp(mxcUrl, size, size, 'crop', undefined, false, useAuthentication) ?? undefined
- : undefined;
+
+ if (!mxcUrl) {
+ return getRoomAvatarUrl(mx, room, size, useAuthentication);
+ }
+
+ return (
+ mx.mxcUrlToHttp(mxcUrl, size, size, 'crop', undefined, false, useAuthentication) ?? undefined
+ );
};
export const trimReplyFromBody = (body: string): string => {
@@ -443,3 +451,103 @@ 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;
+ });
+
+export const getAllVersionsRoomCreator = (room: Room): Set => {
+ const creators = new Set();
+
+ const createEvent = getStateEvent(room, StateEvent.RoomCreate);
+ const createContent = createEvent?.getContent();
+ const creator = createEvent?.getSender();
+ if (typeof creator === 'string') creators.add(creator);
+
+ if (createContent && Array.isArray(createContent.additional_creators)) {
+ createContent.additional_creators.forEach((c) => {
+ if (typeof c === 'string') creators.add(c);
+ });
+ }
+
+ return creators;
+};
+
+export const guessPerfectParent = (
+ mx: MatrixClient,
+ roomId: string,
+ parents: string[]
+): string | undefined => {
+ if (parents.length === 1) {
+ return parents[0];
+ }
+
+ const getSpecialUsers = (rId: string): string[] => {
+ const specialUsers: Set = new Set();
+
+ const r = mx.getRoom(rId);
+ if (!r) return [];
+
+ getAllVersionsRoomCreator(r).forEach((c) => specialUsers.add(c));
+
+ const powerLevels = getStateEvent(
+ r,
+ StateEvent.RoomPowerLevels
+ )?.getContent();
+
+ const { users_default: usersDefault, users } = powerLevels ?? {};
+ const defaultPower = typeof usersDefault === 'number' ? usersDefault : 0;
+
+ if (typeof users === 'object')
+ Object.keys(users).forEach((userId) => {
+ if (users[userId] > defaultPower) {
+ specialUsers.add(userId);
+ }
+ });
+
+ return Array.from(specialUsers);
+ };
+
+ let perfectParent: string | undefined;
+ let score = 0;
+
+ const roomSpecialUsers = getSpecialUsers(roomId);
+ parents.forEach((parentId) => {
+ const parentSpecialUsers = getSpecialUsers(parentId);
+ const matchedUsersCount = parentSpecialUsers.filter((userId) =>
+ roomSpecialUsers.includes(userId)
+ ).length;
+
+ if (matchedUsersCount > score) {
+ score = matchedUsersCount;
+ perfectParent = parentId;
+ }
+ });
+
+ return perfectParent;
+};
diff --git a/src/app/utils/time.ts b/src/app/utils/time.ts
index 3ee6720c..8f1e30e9 100644
--- a/src/app/utils/time.ts
+++ b/src/app/utils/time.ts
@@ -9,12 +9,29 @@ export const today = (ts: number): boolean => dayjs(ts).isToday();
export const yesterday = (ts: number): boolean => dayjs(ts).isYesterday();
-export const timeHourMinute = (ts: number): string => dayjs(ts).format('hh:mm A');
+export const timeHour = (ts: number, hour24Clock: boolean): string =>
+ dayjs(ts).format(hour24Clock ? 'HH' : 'hh');
+export const timeMinute = (ts: number): string => dayjs(ts).format('mm');
+export const timeAmPm = (ts: number): string => dayjs(ts).format('A');
+export const timeDay = (ts: number): string => dayjs(ts).format('D');
+export const timeMon = (ts: number): string => dayjs(ts).format('MMM');
+export const timeMonth = (ts: number): string => dayjs(ts).format('MMMM');
+export const timeYear = (ts: number): string => dayjs(ts).format('YYYY');
-export const timeDayMonYear = (ts: number): string => dayjs(ts).format('D MMM YYYY');
+export const timeHourMinute = (ts: number, hour24Clock: boolean): string =>
+ dayjs(ts).format(hour24Clock ? 'HH:mm' : 'hh:mm A');
+
+export const timeDayMonYear = (ts: number, dateFormatString: string): string =>
+ dayjs(ts).format(dateFormatString);
export const timeDayMonthYear = (ts: number): string => dayjs(ts).format('D MMMM YYYY');
+export const daysInMonth = (month: number, year: number): number =>
+ dayjs(`${year}-${month}-1`).daysInMonth();
+
+export const dateFor = (year: number, month: number, day: number): number =>
+ dayjs(`${year}-${month}-${day}`).valueOf();
+
export const inSameDay = (ts1: number, ts2: number): boolean => {
const dt1 = new Date(ts1);
const dt2 = new Date(ts2);
@@ -33,3 +50,37 @@ export const minuteDifference = (ts1: number, ts2: number): number => {
diff /= 60;
return Math.abs(Math.round(diff));
};
+
+export const hour24to12 = (hour24: number): number => {
+ const h = hour24 % 12;
+
+ if (h === 0) return 12;
+ return h;
+};
+
+export const hour12to24 = (hour: number, pm: boolean): number => {
+ if (hour === 12) {
+ return pm ? 12 : 0;
+ }
+ return pm ? hour + 12 : hour;
+};
+
+export const secondsToMs = (seconds: number) => seconds * 1000;
+
+export const minutesToMs = (minutes: number) => minutes * secondsToMs(60);
+
+export const hoursToMs = (hour: number) => hour * minutesToMs(60);
+
+export const daysToMs = (days: number) => days * hoursToMs(24);
+
+export const getToday = () => {
+ const nowTs = Date.now();
+ const date = dayjs(nowTs);
+ return dateFor(date.year(), date.month() + 1, date.date());
+};
+
+export const getYesterday = () => {
+ const nowTs = Date.now() - daysToMs(1);
+ const date = dayjs(nowTs);
+ return dateFor(date.year(), date.month() + 1, date.date());
+};
diff --git a/src/client/action/room.js b/src/client/action/room.js
index 90b74810..e39aeed8 100644
--- a/src/client/action/room.js
+++ b/src/client/action/room.js
@@ -12,7 +12,7 @@ function addRoomToMDirect(mx, roomId, userId) {
const mDirectsEvent = mx.getAccountData('m.direct');
let userIdToRoomIds = {};
- if (typeof mDirectsEvent !== 'undefined') userIdToRoomIds = mDirectsEvent.getContent();
+ if (typeof mDirectsEvent !== 'undefined') userIdToRoomIds = structuredClone(mDirectsEvent.getContent());
// remove it from the lists of any others users
// (it can only be a DM room for one person)
@@ -93,11 +93,8 @@ function convertToRoom(mx, roomId) {
* @param {string[]} via
*/
async function join(mx, roomIdOrAlias, isDM = false, via = undefined) {
- const roomIdParts = roomIdOrAlias.split(':');
- const viaServers = via || [roomIdParts[1]];
-
try {
- const resultRoom = await mx.joinRoom(roomIdOrAlias, { viaServers });
+ const resultRoom = await mx.joinRoom(roomIdOrAlias, { viaServers: via });
if (isDM) {
const targetUserId = guessDMRoomTargetId(mx.getRoom(resultRoom.roomId), mx.getUserId());
diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts
index 7c774cf1..b80a080f 100644
--- a/src/client/initMatrix.ts
+++ b/src/client/initMatrix.ts
@@ -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 => {
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();
};
diff --git a/src/client/state/cons.js b/src/client/state/cons.js
index c79229b9..6f4fabe4 100644
--- a/src/client/state/cons.js
+++ b/src/client/state/cons.js
@@ -1,5 +1,5 @@
const cons = {
- version: '4.6.0',
+ version: '4.9.1',
secretKey: {
ACCESS_TOKEN: 'cinny_access_token',
DEVICE_ID: 'cinny_device_id',
diff --git a/src/types/matrix/accountData.ts b/src/types/matrix/accountData.ts
index 1c6eee89..b9d81492 100644
--- a/src/types/matrix/accountData.ts
+++ b/src/types/matrix/accountData.ts
@@ -19,6 +19,8 @@ export enum AccountDataEvent {
MegolmBackupV1 = 'm.megolm_backup.v1',
}
+export type MDirectContent = Record;
+
export type SecretStorageDefaultKeyContent = {
key: string;
};
diff --git a/src/types/matrix/room.ts b/src/types/matrix/room.ts
index 65dc35f4..b866fd77 100644
--- a/src/types/matrix/room.ts
+++ b/src/types/matrix/room.ts
@@ -1,3 +1,5 @@
+import { IImageInfo } from './common';
+
export enum Membership {
Invite = 'invite',
Knock = 'knock',
@@ -68,8 +70,9 @@ export type IRoomCreateContent = {
['m.federate']?: boolean;
room_version: string;
type?: string;
+ additional_creators?: string[];
predecessor?: {
- event_id: string;
+ event_id?: string;
room_id: string;
};
};
@@ -93,3 +96,13 @@ export type MuteChanges = {
added: string[];
removed: string[];
};
+
+export type MemberPowerTagIcon = {
+ key?: string;
+ info?: IImageInfo;
+};
+export type MemberPowerTag = {
+ name: string;
+ color?: string;
+ icon?: MemberPowerTagIcon;
+};
diff --git a/vite.config.js b/vite.config.js
index 9c8d88bc..dfa02fc4 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -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',