From d8833a310d6980b0a08237b9f58b1014d6ca2b1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:59:12 +1000 Subject: [PATCH 01/11] Bump cla-assistant/github-action from 2.5.1 to 2.6.1 (#1987) Bumps [cla-assistant/github-action](https://github.com/cla-assistant/github-action) from 2.5.1 to 2.6.1. - [Release notes](https://github.com/cla-assistant/github-action/releases) - [Commits](https://github.com/cla-assistant/github-action/compare/v2.5.1...v2.6.1) --- updated-dependencies: - dependency-name: cla-assistant/github-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cla.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 65513427..29fe7eb2 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -12,7 +12,7 @@ jobs: - name: 'CLA Assistant' if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' # Beta Release - uses: cla-assistant/github-action@v2.5.1 + uses: cla-assistant/github-action@v2.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # the below token should have repo scope and must be manually added by you in the repository's secret From c38efdfbce03b21fad2fd44b7ac939b17057a80c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:59:55 +1000 Subject: [PATCH 02/11] Bump docker/build-push-action from 6.7.0 to 6.9.0 (#1986) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.7.0 to 6.9.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6.7.0...v6.9.0) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-pr.yml | 2 +- .github/workflows/prod-deploy.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml index 7c67c27a..77d0dcf9 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.1.7 - name: Build Docker image - uses: docker/build-push-action@v6.7.0 + uses: docker/build-push-action@v6.9.0 with: context: . push: false diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml index f8f13a7e..8aea3662 100644 --- a/.github/workflows/prod-deploy.yml +++ b/.github/workflows/prod-deploy.yml @@ -90,7 +90,7 @@ jobs: ${{ secrets.DOCKER_USERNAME }}/cinny ghcr.io/${{ github.repository }} - name: Build and push Docker image - uses: docker/build-push-action@v6.7.0 + uses: docker/build-push-action@v6.9.0 with: context: . platforms: linux/amd64,linux/arm64 From 48265c4227102794b48955e15246d70db73ef62a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:00:32 +1000 Subject: [PATCH 03/11] Bump actions/checkout from 4.1.7 to 4.2.0 (#1985) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 4.2.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.7...v4.2.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pull-request.yml | 2 +- .github/workflows/docker-pr.yml | 2 +- .github/workflows/lockfile.yml | 2 +- .github/workflows/netlify-dev.yml | 2 +- .github/workflows/prod-deploy.yml | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index ae1097be..a73d0d39 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -12,7 +12,7 @@ jobs: PR_NUMBER: ${{github.event.number}} steps: - name: Checkout repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Setup node uses: actions/setup-node@v4.0.3 with: diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml index 77d0dcf9..b44f79e3 100644 --- a/.github/workflows/docker-pr.yml +++ b/.github/workflows/docker-pr.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Build Docker image uses: docker/build-push-action@v6.9.0 with: diff --git a/.github/workflows/lockfile.yml b/.github/workflows/lockfile.yml index 410d2f59..be52eb50 100644 --- a/.github/workflows/lockfile.yml +++ b/.github/workflows/lockfile.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - name: Checkout - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: NPM Lockfile Changes uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891 with: diff --git a/.github/workflows/netlify-dev.yml b/.github/workflows/netlify-dev.yml index 71d34294..3145e046 100644 --- a/.github/workflows/netlify-dev.yml +++ b/.github/workflows/netlify-dev.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Setup node uses: actions/setup-node@v4.0.3 with: diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml index 8aea3662..fef8ead5 100644 --- a/.github/workflows/prod-deploy.yml +++ b/.github/workflows/prod-deploy.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Setup node uses: actions/setup-node@v4.0.3 with: @@ -66,7 +66,7 @@ jobs: packages: write steps: - name: Checkout repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Set up QEMU uses: docker/setup-qemu-action@v3.2.0 - name: Set up Docker Buildx From cca8b5f2b2bed3241c3f0de60a4efae2798a2d9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:03:33 +1000 Subject: [PATCH 04/11] Bump actions/setup-node from 4.0.3 to 4.0.4 (#1969) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.3 to 4.0.4. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4.0.3...v4.0.4) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pull-request.yml | 2 +- .github/workflows/netlify-dev.yml | 2 +- .github/workflows/prod-deploy.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index a73d0d39..c42f339b 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.0.3 + uses: actions/setup-node@v4.0.4 with: node-version: 20.12.2 cache: 'npm' diff --git a/.github/workflows/netlify-dev.yml b/.github/workflows/netlify-dev.yml index 3145e046..08a66128 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.0.3 + uses: actions/setup-node@v4.0.4 with: node-version: 20.12.2 cache: 'npm' diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml index fef8ead5..5f618c3e 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.0.3 + uses: actions/setup-node@v4.0.4 with: node-version: 20.12.2 cache: 'npm' From 35b0b1ea424b8b23bfee9fa9b26179da5b0da443 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:22:06 +1100 Subject: [PATCH 05/11] fix(deps): update dependency matrix-js-sdk to v34.8.0 (#2011) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index d2c7a617..66f11caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "jotai": "2.6.0", "linkify-react": "4.1.3", "linkifyjs": "4.1.3", - "matrix-js-sdk": "34.5.0", + "matrix-js-sdk": "34.8.0", "millify": "6.1.0", "pdfjs-dist": "4.2.67", "prismjs": "1.29.0", @@ -2646,9 +2646,9 @@ } }, "node_modules/@matrix-org/matrix-sdk-crypto-wasm": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-7.0.0.tgz", - "integrity": "sha512-MOencXiW/gI5MuTtCNsuojjwT5DXCrjMqv9xOslJC9h2tPdLFFFMGr58dY5Lis4DRd9MRWcgrGowUIHOqieWTA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz", + "integrity": "sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og==", "license": "Apache-2.0", "engines": { "node": ">= 10" @@ -9117,13 +9117,13 @@ "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" }, "node_modules/matrix-js-sdk": { - "version": "34.5.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.5.0.tgz", - "integrity": "sha512-pbp+IxAkSwGmefrlUGCrtrs3UWyqN2iWh4lKnJW+jFIlsksXq7A8vL4cS1z8LXmpcHQAg3mKNuj8n8uhm51t1A==", + "version": "34.8.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.8.0.tgz", + "integrity": "sha512-zHYPE+vb6PP/dnRa6X3ktXf34yQg/Pct9r5CSP263hA4tRg5sKEVQgnY7KDADaABTvNMb9h7BZ5Xm9vjXsowNQ==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^7.0.0", + "@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0", "@matrix-org/olm": "3.2.15", "another-json": "^0.2.0", "bs58": "^6.0.0", diff --git a/package.json b/package.json index 118f9640..29bc11a2 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "jotai": "2.6.0", "linkify-react": "4.1.3", "linkifyjs": "4.1.3", - "matrix-js-sdk": "34.5.0", + "matrix-js-sdk": "34.8.0", "millify": "6.1.0", "pdfjs-dist": "4.2.67", "prismjs": "1.29.0", From 0e51e19cab5ea616c6477b368f23fa9a1796671f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=9C=E5=9D=82=E9=9B=85?= <23130178+ShadowRZ@users.noreply.github.com> Date: Wed, 16 Oct 2024 18:26:03 +0800 Subject: [PATCH 06/11] fix: register service worker immediately and cache media requests (#1977) * Allow service worker to immediately claim pages * Allow media requests to be cached by browser --- src/sw.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sw.ts b/src/sw.ts index 11f7f8b2..2179dfcb 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -23,9 +23,14 @@ function fetchConfig(token?: string): RequestInit | undefined { headers: { Authorization: `Bearer ${token}`, }, + cache: 'default', }; } +self.addEventListener('activate', (event: ExtendableEvent) => { + event.waitUntil(clients.claim()); +}); + self.addEventListener('fetch', (event: FetchEvent) => { const { url, method } = event.request; if (method !== 'GET') return; From c110e64341eef37706c5befb9fe8858b5e62bd5c Mon Sep 17 00:00:00 2001 From: Krishan <33421343+kfiven@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:29:30 +1100 Subject: [PATCH 07/11] Release v4.2.2 (#2012) --- package-lock.json | 4 ++-- package.json | 2 +- src/app/pages/auth/AuthFooter.tsx | 2 +- src/app/pages/client/WelcomePage.tsx | 2 +- src/client/state/cons.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 66f11caa..1441efb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cinny", - "version": "4.2.1", + "version": "4.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cinny", - "version": "4.2.1", + "version": "4.2.2", "license": "AGPL-3.0-only", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "1.1.6", diff --git a/package.json b/package.json index 29bc11a2..07264050 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cinny", - "version": "4.2.1", + "version": "4.2.2", "description": "Yet another matrix client", "main": "index.js", "type": "module", diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx index d555ef0e..40953294 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.2.1 + v4.2.2 Twitter diff --git a/src/app/pages/client/WelcomePage.tsx b/src/app/pages/client/WelcomePage.tsx index 161167e2..d7a48e44 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.2.1 + v4.2.2 } diff --git a/src/client/state/cons.js b/src/client/state/cons.js index fbad4687..898670c1 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -1,5 +1,5 @@ const cons = { - version: '4.2.1', + version: '4.2.2', secretKey: { ACCESS_TOKEN: 'cinny_access_token', DEVICE_ID: 'cinny_device_id', From 492a149c7f5c96b1ec20e98d74dfc00fbb9b51d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:43:50 +1100 Subject: [PATCH 08/11] fix(deps): update dependency matrix-js-sdk to v34.11.1 (#2053) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1441efb4..e7ae098c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "jotai": "2.6.0", "linkify-react": "4.1.3", "linkifyjs": "4.1.3", - "matrix-js-sdk": "34.8.0", + "matrix-js-sdk": "34.11.1", "millify": "6.1.0", "pdfjs-dist": "4.2.67", "prismjs": "1.29.0", @@ -9117,9 +9117,9 @@ "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" }, "node_modules/matrix-js-sdk": { - "version": "34.8.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.8.0.tgz", - "integrity": "sha512-zHYPE+vb6PP/dnRa6X3ktXf34yQg/Pct9r5CSP263hA4tRg5sKEVQgnY7KDADaABTvNMb9h7BZ5Xm9vjXsowNQ==", + "version": "34.11.1", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.11.1.tgz", + "integrity": "sha512-rDbIUIqEsN/pbHb6haBQmjxxgeb9G3Df2IhPPOotUbX6R1KseA8yJ6TAY0YySM2zVaBV3yZ6dnKWexF/uWvZfA==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.12.5", diff --git a/package.json b/package.json index 07264050..34720f86 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "jotai": "2.6.0", "linkify-react": "4.1.3", "linkifyjs": "4.1.3", - "matrix-js-sdk": "34.8.0", + "matrix-js-sdk": "34.11.1", "millify": "6.1.0", "pdfjs-dist": "4.2.67", "prismjs": "1.29.0", From a142630ff970e47303fb0aeff0f7cad6f6fbdaf3 Mon Sep 17 00:00:00 2001 From: Krishan <33421343+kfiven@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:45:34 +1100 Subject: [PATCH 09/11] Release v4.2.3 (#2052) --- package-lock.json | 4 ++-- package.json | 2 +- src/app/pages/auth/AuthFooter.tsx | 2 +- src/app/pages/client/WelcomePage.tsx | 2 +- src/client/state/cons.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index e7ae098c..7fb554e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cinny", - "version": "4.2.2", + "version": "4.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cinny", - "version": "4.2.2", + "version": "4.2.3", "license": "AGPL-3.0-only", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "1.1.6", diff --git a/package.json b/package.json index 34720f86..1ecbc190 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cinny", - "version": "4.2.2", + "version": "4.2.3", "description": "Yet another matrix client", "main": "index.js", "type": "module", diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx index 40953294..2ad02057 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.2.2 + v4.2.3 Twitter diff --git a/src/app/pages/client/WelcomePage.tsx b/src/app/pages/client/WelcomePage.tsx index d7a48e44..3aeffa1f 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.2.2 + v4.2.3 } diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 898670c1..0b225ede 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -1,5 +1,5 @@ const cons = { - version: '4.2.2', + version: '4.2.3', secretKey: { ACCESS_TOKEN: 'cinny_access_token', DEVICE_ID: 'cinny_device_id', From 00d5553bcbf674d70bfc7c295c730c86690451a9 Mon Sep 17 00:00:00 2001 From: Rein Fernhout Date: Fri, 13 Dec 2024 05:32:25 +0100 Subject: [PATCH 10/11] add tableflip and unflip commands (#2075) --- src/app/features/room/RoomInput.tsx | 8 +++++++- src/app/hooks/useCommands.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 8fcfbc97..4d43c7e9 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -103,7 +103,7 @@ import { } from '../../utils/room'; import { sanitizeText } from '../../utils/sanitize'; import { CommandAutocomplete } from './CommandAutocomplete'; -import { Command, SHRUG, useCommands } from '../../hooks/useCommands'; +import { Command, SHRUG, TABLEFLIP, UNFLIP, useCommands } from '../../hooks/useCommands'; import { mobileOrTablet } from '../../utils/user-agent'; import { useElementSizeObserver } from '../../hooks/useElementSizeObserver'; import { ReplyLayout, ThreadIndicator } from '../../components/message'; @@ -270,6 +270,12 @@ export const RoomInput = forwardRef( } else if (commandName === Command.Shrug) { plainText = `${SHRUG} ${plainText}`; customHtml = `${SHRUG} ${customHtml}`; + } else if (commandName === Command.TableFlip) { + plainText = `${TABLEFLIP} ${plainText}`; + customHtml = `${TABLEFLIP} ${customHtml}`; + } else if (commandName === Command.UnFlip) { + plainText = `${UNFLIP} ${plainText}`; + customHtml = `${UNFLIP} ${customHtml}`; } else if (commandName) { const commandContent = commands[commandName as Command]; if (commandContent) { diff --git a/src/app/hooks/useCommands.ts b/src/app/hooks/useCommands.ts index 182d1307..bc7d2892 100644 --- a/src/app/hooks/useCommands.ts +++ b/src/app/hooks/useCommands.ts @@ -6,6 +6,8 @@ import * as roomActions from '../../client/action/room'; import { useRoomNavigate } from './useRoomNavigate'; export const SHRUG = '¯\\_(ツ)_/¯'; +export const TABLEFLIP = '(╯°□°)╯︵ ┻━┻'; +export const UNFLIP = '┬─┬ノ( º_ºノ)'; export function parseUsersAndReason(payload: string): { users: string[]; @@ -48,6 +50,8 @@ export enum Command { MyRoomAvatar = 'myroomavatar', ConvertToDm = 'converttodm', ConvertToRoom = 'converttoroom', + TableFlip = 'tableflip', + UnFlip = 'unflip', } export type CommandContent = { @@ -78,6 +82,16 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { description: 'Send ¯\\_(ツ)_/¯ as message', exe: async () => undefined, }, + [Command.TableFlip]: { + name: Command.TableFlip, + description: `Send ${TABLEFLIP} as message`, + exe: async () => undefined, + }, + [Command.UnFlip]: { + name: Command.UnFlip, + description: `Send ${UNFLIP} as message`, + exe: async () => undefined, + }, [Command.StartDm]: { name: Command.StartDm, description: 'Start direct message with user. Example: /startdm userId1', From 35f0e400ad2e4ca1e7f5757f63843ac21b3c5d08 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 16 Dec 2024 21:55:15 +1100 Subject: [PATCH 11/11] Pinned Messages (#2081) * add pinned room events hook * room pinned message - WIP * add room event hook * fetch pinned messages before displaying * use react-query in room event hook * disable staleTime and gc to 1 hour in room event hook * use room event hook in reply component * render pinned messages * add option to pin/unpin messages * remove message base from message placeholders and add variant * display message placeholder while loading pinned messages * render pinned event error * show no pinned message placeholder * fix message placeholder flickering --- src/app/components/message/Reply.tsx | 130 ++--- .../placeholder/CompactPlaceholder.tsx | 43 +- .../placeholder/DefaultPlaceholder.tsx | 52 +- .../placeholder/LinePlaceholder.css.ts | 43 +- .../message/placeholder/LinePlaceholder.tsx | 13 +- src/app/features/room/RoomTimeline.tsx | 73 ++- src/app/features/room/RoomViewHeader.tsx | 69 ++- src/app/features/room/message/Message.tsx | 141 ++++-- .../room/room-pin-menu/RoomPinMenu.css.ts | 18 + .../room/room-pin-menu/RoomPinMenu.tsx | 468 ++++++++++++++++++ src/app/features/room/room-pin-menu/index.ts | 1 + src/app/hooks/useRoomEvent.ts | 56 +++ src/app/hooks/useRoomPinnedEvents.ts | 15 + src/app/pages/client/inbox/Notifications.tsx | 10 +- 14 files changed, 940 insertions(+), 192 deletions(-) create mode 100644 src/app/features/room/room-pin-menu/RoomPinMenu.css.ts create mode 100644 src/app/features/room/room-pin-menu/RoomPinMenu.tsx create mode 100644 src/app/features/room/room-pin-menu/index.ts create mode 100644 src/app/hooks/useRoomEvent.ts create mode 100644 src/app/hooks/useRoomPinnedEvents.ts diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index 82a9d919..7687074e 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -1,8 +1,6 @@ import { Box, Icon, Icons, Text, as, color, toRem } from 'folds'; -import { EventTimelineSet, MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk'; -import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; -import React, { MouseEventHandler, ReactNode, useEffect, useMemo, useState } from 'react'; -import to from 'await-to-js'; +import { EventTimelineSet, Room } from 'matrix-js-sdk'; +import React, { MouseEventHandler, ReactNode, useCallback, useMemo } from 'react'; import classNames from 'classnames'; import colorMXID from '../../../util/colorMXID'; import { getMemberDisplayName, trimReplyFromBody } from '../../utils/room'; @@ -12,6 +10,7 @@ import { randomNumberBetween } from '../../utils/common'; import * as css from './Reply.css'; import { MessageBadEncryptedContent, MessageDeletedContent, MessageFailedContent } from './content'; import { scaleSystemEmoji } from '../../plugins/react-custom-html-parser'; +import { useRoomEvent } from '../../hooks/useRoomEvent'; type ReplyLayoutProps = { userColor?: string; @@ -46,7 +45,6 @@ export const ThreadIndicator = as<'div'>(({ ...props }, ref) => ( )); type ReplyProps = { - mx: MatrixClient; room: Room; timelineSet?: EventTimelineSet | undefined; replyEventId: string; @@ -54,78 +52,60 @@ type ReplyProps = { onClick?: MouseEventHandler | undefined; }; -export const Reply = as<'div', ReplyProps>((_, ref) => { - const { mx, room, timelineSet, replyEventId, threadRootId, onClick, ...props } = _; - const [replyEvent, setReplyEvent] = useState( - timelineSet?.findEventById(replyEventId) - ); - const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []); +export const Reply = as<'div', ReplyProps>( + ({ room, timelineSet, replyEventId, threadRootId, onClick, ...props }, ref) => { + const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []); + const getFromLocalTimeline = useCallback( + () => timelineSet?.findEventById(replyEventId), + [timelineSet, replyEventId] + ); + const replyEvent = useRoomEvent(room, replyEventId, getFromLocalTimeline); - const { body } = replyEvent?.getContent() ?? {}; - const sender = replyEvent?.getSender(); + const { body } = replyEvent?.getContent() ?? {}; + const sender = replyEvent?.getSender(); - const fallbackBody = replyEvent?.isRedacted() ? ( - - ) : ( - - ); + const fallbackBody = replyEvent?.isRedacted() ? ( + + ) : ( + + ); - useEffect(() => { - let disposed = false; - const loadEvent = async () => { - const [err, evt] = await to(mx.fetchRoomEvent(room.roomId, replyEventId)); - const mEvent = new MatrixEvent(evt); - if (disposed) return; - if (err) { - setReplyEvent(null); - return; - } - if (mEvent.isEncrypted() && mx.getCrypto()) { - await to(mEvent.attemptDecryption(mx.getCrypto() as CryptoBackend)); - } - setReplyEvent(mEvent); - }; - if (replyEvent === undefined) loadEvent(); - return () => { - disposed = true; - }; - }, [replyEvent, mx, room, replyEventId]); + const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted'; + const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody; - const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted'; - const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody; - - return ( - - {threadRootId && ( - - )} - - {getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender)} - - ) - } - data-event-id={replyEventId} - onClick={onClick} - > - {replyEvent !== undefined ? ( - - {badEncryption ? : bodyJSX} - - ) : ( - + return ( + + {threadRootId && ( + )} - - - ); -}); + + {getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender)} + + ) + } + data-event-id={replyEventId} + onClick={onClick} + > + {replyEvent !== undefined ? ( + + {badEncryption ? : bodyJSX} + + ) : ( + + )} + + + ); + } +); diff --git a/src/app/components/message/placeholder/CompactPlaceholder.tsx b/src/app/components/message/placeholder/CompactPlaceholder.tsx index a6be083e..e6168ae3 100644 --- a/src/app/components/message/placeholder/CompactPlaceholder.tsx +++ b/src/app/components/message/placeholder/CompactPlaceholder.tsx @@ -1,22 +1,27 @@ -import React from 'react'; -import { as, toRem } from 'folds'; +import React, { useMemo } from 'react'; +import { as, ContainerColor, toRem } from 'folds'; import { randomNumberBetween } from '../../../utils/common'; import { LinePlaceholder } from './LinePlaceholder'; -import { CompactLayout, MessageBase } from '../layout'; +import { CompactLayout } from '../layout'; -export const CompactPlaceholder = as<'div'>(({ ...props }, ref) => ( - - - - - - } - > - - - -)); +export const CompactPlaceholder = as<'div', { variant?: ContainerColor }>( + ({ variant, ...props }, ref) => { + const nameSize = useMemo(() => randomNumberBetween(40, 100), []); + const msgSize = useMemo(() => randomNumberBetween(120, 500), []); + + return ( + + + + + } + > + + + ); + } +); diff --git a/src/app/components/message/placeholder/DefaultPlaceholder.tsx b/src/app/components/message/placeholder/DefaultPlaceholder.tsx index 5f0b57fa..725ac4b9 100644 --- a/src/app/components/message/placeholder/DefaultPlaceholder.tsx +++ b/src/app/components/message/placeholder/DefaultPlaceholder.tsx @@ -1,25 +1,39 @@ -import React, { CSSProperties } from 'react'; -import { Avatar, Box, as, color, toRem } from 'folds'; +import React, { CSSProperties, useMemo } from 'react'; +import { Avatar, Box, ContainerColor, as, color, toRem } from 'folds'; import { randomNumberBetween } from '../../../utils/common'; import { LinePlaceholder } from './LinePlaceholder'; -import { MessageBase, ModernLayout } from '../layout'; +import { ModernLayout } from '../layout'; const contentMargin: CSSProperties = { marginTop: toRem(3) }; -const avatarBg: CSSProperties = { backgroundColor: color.SurfaceVariant.Container }; -export const DefaultPlaceholder = as<'div'>(({ ...props }, ref) => ( - - }> - - - - +export const DefaultPlaceholder = as<'div', { variant?: ContainerColor }>( + ({ variant, ...props }, ref) => { + const nameSize = useMemo(() => randomNumberBetween(40, 100), []); + const msgSize = useMemo(() => randomNumberBetween(80, 200), []); + const msg2Size = useMemo(() => randomNumberBetween(80, 200), []); + + return ( + + } + > + + + + + + + + + - - - - - - - -)); + + ); + } +); diff --git a/src/app/components/message/placeholder/LinePlaceholder.css.ts b/src/app/components/message/placeholder/LinePlaceholder.css.ts index 0baedf6e..34ad76a3 100644 --- a/src/app/components/message/placeholder/LinePlaceholder.css.ts +++ b/src/app/components/message/placeholder/LinePlaceholder.css.ts @@ -1,12 +1,35 @@ -import { style } from '@vanilla-extract/css'; -import { DefaultReset, color, config, toRem } from 'folds'; +import { ComplexStyleRule } from '@vanilla-extract/css'; +import { recipe, RecipeVariants } from '@vanilla-extract/recipes'; +import { ContainerColor, DefaultReset, color, config, toRem } from 'folds'; -export const LinePlaceholder = style([ - DefaultReset, - { - width: '100%', - height: toRem(16), - borderRadius: config.radii.R300, - backgroundColor: color.SurfaceVariant.Container, +const getVariant = (variant: ContainerColor): ComplexStyleRule => ({ + backgroundColor: color[variant].Container, +}); + +export const LinePlaceholder = recipe({ + base: [ + DefaultReset, + { + width: '100%', + height: toRem(16), + borderRadius: config.radii.R300, + }, + ], + variants: { + variant: { + Background: getVariant('Background'), + Surface: getVariant('Surface'), + SurfaceVariant: getVariant('SurfaceVariant'), + Primary: getVariant('Primary'), + Secondary: getVariant('Secondary'), + Success: getVariant('Success'), + Warning: getVariant('Warning'), + Critical: getVariant('Critical'), + }, }, -]); + defaultVariants: { + variant: 'SurfaceVariant', + }, +}); + +export type LinePlaceholderVariants = RecipeVariants; diff --git a/src/app/components/message/placeholder/LinePlaceholder.tsx b/src/app/components/message/placeholder/LinePlaceholder.tsx index a5e7bd75..58fc52c0 100644 --- a/src/app/components/message/placeholder/LinePlaceholder.tsx +++ b/src/app/components/message/placeholder/LinePlaceholder.tsx @@ -3,6 +3,13 @@ import { Box, as } from 'folds'; import classNames from 'classnames'; import * as css from './LinePlaceholder.css'; -export const LinePlaceholder = as<'div'>(({ className, ...props }, ref) => ( - -)); +export const LinePlaceholder = as<'div', css.LinePlaceholderVariants>( + ({ className, variant, ...props }, ref) => ( + + ) +); diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index a2738fcb..63b3d3e2 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -433,10 +433,12 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents'); const setReplyDraft = useSetAtom(roomIdToReplyDraftAtomFamily(room.roomId)); const powerLevels = usePowerLevelsContext(); - const { canDoAction, canSendEvent, getPowerLevel } = usePowerLevelsAPI(powerLevels); + const { canDoAction, canSendEvent, canSendStateEvent, getPowerLevel } = + usePowerLevelsAPI(powerLevels); const myPowerLevel = getPowerLevel(mx.getUserId() ?? ''); const canRedact = canDoAction('redact', myPowerLevel); const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel); + const canPinEvent = canSendStateEvent(StateEvent.RoomPinnedEvents, myPowerLevel); const [editId, setEditId] = useState(); const roomToParents = useAtomValue(roomToParentsAtom); const unread = useRoomUnread(room.roomId, roomToUnreadAtom); @@ -983,6 +985,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli edit={editId === mEventId} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} canSendReaction={canSendReaction} + canPinEvent={canPinEvent} imagePackRooms={imagePackRooms} relations={hasReactions ? reactionRelations : undefined} onUserClick={handleUserClick} @@ -993,7 +996,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli reply={ replyEventId && ( - - - - - + + + + + + + + + + + + + + + ) : ( <> - - - + + + + + + + + + ))} @@ -1570,17 +1589,33 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli {(!liveTimelineLinked || !rangeAtEnd) && (messageLayout === 1 ? ( <> - - - - - + + + + + + + + + + + + + + + ) : ( <> - - - + + + + + + + + + ))} diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index ae80deb6..7ee1d302 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -19,6 +19,7 @@ import { Line, PopOut, RectCords, + Badge, } from 'folds'; import { useNavigate } from 'react-router-dom'; import { JoinRule, Room } from 'matrix-js-sdk'; @@ -54,6 +55,8 @@ import { getMatrixToRoom } from '../../plugins/matrix-to'; import { getViaServers } from '../../plugins/via-servers'; import { BackRouteHandler } from '../../components/BackRouteHandler'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; +import { useRoomPinnedEvents } from '../../hooks/useRoomPinnedEvents'; +import { RoomPinMenu } from './room-pin-menu'; type RoomMenuProps = { room: Room; @@ -180,14 +183,18 @@ export function RoomViewHeader() { const room = useRoom(); const space = useSpaceOptionally(); const [menuAnchor, setMenuAnchor] = useState(); + const [pinMenuAnchor, setPinMenuAnchor] = useState(); const mDirects = useAtomValue(mDirectAtom); + const pinnedEvents = useRoomPinnedEvents(room); const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption); const ecryptedRoom = !!encryptionEvent; const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId)); const name = useRoomName(room); const topic = useRoomTopic(room); - const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined; + const avatarUrl = avatarMxc + ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined + : undefined; const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); @@ -205,6 +212,10 @@ export function RoomViewHeader() { setMenuAnchor(evt.currentTarget.getBoundingClientRect()); }; + const handleOpenPinMenu: MouseEventHandler = (evt) => { + setPinMenuAnchor(evt.currentTarget.getBoundingClientRect()); + }; + return ( @@ -297,6 +308,62 @@ export function RoomViewHeader() { )} )} + + Pinned Messages + + } + > + {(triggerRef) => ( + + {pinnedEvents.length > 0 && ( + + + {pinnedEvents.length} + + + )} + + + )} + + setPinMenuAnchor(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', + isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', + escapeDeactivates: stopPropagation, + }} + > + setPinMenuAnchor(undefined)} /> + + } + /> {screenSize === ScreenSize.Desktop && ( void; @@ -235,9 +243,9 @@ export const MessageSourceCodeItem = as< const getContent = (evt: MatrixEvent) => evt.isEncrypted() ? { - [`<== DECRYPTED_EVENT ==>`]: evt.getEffectiveEvent(), - [`<== ORIGINAL_EVENT ==>`]: evt.event, - } + [`<== DECRYPTED_EVENT ==>`]: evt.getEffectiveEvent(), + [`<== ORIGINAL_EVENT ==>`]: evt.event, + } : evt.event; const getText = (): string => { @@ -340,6 +348,46 @@ export const MessageCopyLinkItem = as< ); }); +export const MessagePinItem = as< + 'button', + { + room: Room; + mEvent: MatrixEvent; + onClose?: () => void; + } +>(({ room, mEvent, onClose, ...props }, ref) => { + const mx = useMatrixClient(); + const pinnedEvents = useRoomPinnedEvents(room); + const isPinned = pinnedEvents.includes(mEvent.getId() ?? ''); + + const handlePin = () => { + const eventId = mEvent.getId(); + const pinContent: RoomPinnedEventsEventContent = { + pinned: Array.from(pinnedEvents).filter((id) => id !== eventId), + }; + if (!isPinned && eventId) { + pinContent.pinned.push(eventId); + } + mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents, pinContent); + onClose?.(); + }; + + return ( + } + radii="300" + onClick={handlePin} + {...props} + ref={ref} + > + + {isPinned ? 'Unpin Message' : 'Pin Message'} + + + ); +}); + export const MessageDeleteItem = as< 'button', { @@ -611,6 +659,7 @@ export type MessageProps = { edit?: boolean; canDelete?: boolean; canSendReaction?: boolean; + canPinEvent?: boolean; imagePackRooms?: Room[]; relations?: Relations; messageLayout: MessageLayout; @@ -634,6 +683,7 @@ export const Message = as<'div', MessageProps>( edit, canDelete, canSendReaction, + canPinEvent, imagePackRooms, relations, messageLayout, @@ -949,29 +999,32 @@ export const Message = as<'div', MessageProps>( /> + {canPinEvent && ( + + )} {((!mEvent.isRedacted() && canDelete) || mEvent.getSender() !== mx.getUserId()) && ( - <> - - - {!mEvent.isRedacted() && canDelete && ( - - )} - {mEvent.getSender() !== mx.getUserId() && ( - - )} - - - )} + <> + + + {!mEvent.isRedacted() && canDelete && ( + + )} + {mEvent.getSender() !== mx.getUserId() && ( + + )} + + + )} } @@ -1095,26 +1148,26 @@ export const Event = as<'div', EventProps>( {((!mEvent.isRedacted() && canDelete && !stateEvent) || (mEvent.getSender() !== mx.getUserId() && !stateEvent)) && ( - <> - - - {!mEvent.isRedacted() && canDelete && ( - - )} - {mEvent.getSender() !== mx.getUserId() && ( - - )} - - - )} + <> + + + {!mEvent.isRedacted() && canDelete && ( + + )} + {mEvent.getSender() !== mx.getUserId() && ( + + )} + + + )} } diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.css.ts b/src/app/features/room/room-pin-menu/RoomPinMenu.css.ts new file mode 100644 index 00000000..9b0269b5 --- /dev/null +++ b/src/app/features/room/room-pin-menu/RoomPinMenu.css.ts @@ -0,0 +1,18 @@ +import { style } from '@vanilla-extract/css'; +import { config, toRem } from 'folds'; + +export const PinMenu = style({ + display: 'flex', + maxWidth: toRem(548), + width: '100vw', + maxHeight: '90vh', +}); + +export const PinMenuHeader = style({ + paddingLeft: config.space.S400, + paddingRight: config.space.S200, +}); + +export const PinMenuContent = style({ + paddingLeft: config.space.S200, +}); diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx new file mode 100644 index 00000000..c3d259ae --- /dev/null +++ b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx @@ -0,0 +1,468 @@ +/* eslint-disable react/destructuring-assignment */ +import React, { forwardRef, MouseEventHandler, useCallback, useMemo, useRef } from 'react'; +import { MatrixEvent, RelationType, Room } from 'matrix-js-sdk'; +import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types'; +import { + Avatar, + Box, + Chip, + color, + config, + Header, + Icon, + IconButton, + Icons, + Menu, + Scroll, + Spinner, + Text, + toRem, +} from 'folds'; +import { Opts as LinkifyOpts } from 'linkifyjs'; +import { HTMLReactParserOptions } from 'html-react-parser'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents'; +import * as css from './RoomPinMenu.css'; +import { SequenceCard } from '../../../components/sequence-card'; +import { useRoomEvent } from '../../../hooks/useRoomEvent'; +import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; +import { + AvatarBase, + DefaultPlaceholder, + ImageContent, + MessageNotDecryptedContent, + MessageUnsupportedContent, + ModernLayout, + MSticker, + RedactedContent, + Reply, + Time, + Username, +} from '../../../components/message'; +import { UserAvatar } from '../../../components/user-avatar'; +import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix'; +import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { + getEditedEvent, + getMemberAvatarMxc, + getMemberDisplayName, + getStateEvent, +} from '../../../utils/room'; +import { GetContentCallback, MessageEvent, StateEvent } from '../../../../types/matrix/room'; +import colorMXID from '../../../../util/colorMXID'; +import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler'; +import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler'; +import { + factoryRenderLinkifyWithMention, + getReactCustomHtmlParser, + LINKIFY_OPTS, + makeMentionCustomProps, + renderMatrixMention, +} from '../../../plugins/react-custom-html-parser'; +import { RenderMatrixEvent, useMatrixEventRenderer } from '../../../hooks/useMatrixEventRenderer'; +import { RenderMessageContent } from '../../../components/RenderMessageContent'; +import { useSetting } from '../../../state/hooks/settings'; +import { settingsAtom } from '../../../state/settings'; +import * as customHtmlCss from '../../../styles/CustomHtml.css'; +import { EncryptedContent } from '../message'; +import { Image } from '../../../components/media'; +import { ImageViewer } from '../../../components/image-viewer'; +import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; +import { VirtualTile } from '../../../components/virtualizer'; +import { usePowerLevelsAPI, usePowerLevelsContext } from '../../../hooks/usePowerLevels'; +import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; +import { ContainerColor } from '../../../styles/ContainerColor.css'; + +type PinnedMessageProps = { + room: Room; + eventId: string; + renderContent: RenderMatrixEvent<[MatrixEvent, string, GetContentCallback]>; + onOpen: (roomId: string, eventId: string) => void; + canPinEvent: boolean; +}; +function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: PinnedMessageProps) { + const pinnedEvent = useRoomEvent(room, eventId); + const useAuthentication = useMediaAuthentication(); + const mx = useMatrixClient(); + + const [unpinState, unpin] = useAsyncCallback( + useCallback(() => { + const pinEvent = getStateEvent(room, StateEvent.RoomPinnedEvents); + const content = pinEvent?.getContent() ?? { pinned: [] }; + const newContent: RoomPinnedEventsEventContent = { + pinned: content.pinned.filter((id) => id !== eventId), + }; + + return mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents, newContent); + }, [room, eventId, mx]) + ); + + const handleOpenClick: MouseEventHandler = (evt) => { + evt.stopPropagation(); + const evtId = evt.currentTarget.getAttribute('data-event-id'); + if (!evtId) return; + onOpen(room.roomId, evtId); + }; + + const handleUnpinClick: MouseEventHandler = (evt) => { + evt.stopPropagation(); + unpin(); + }; + + const renderOptions = () => ( + + + Open + + {canPinEvent && ( + + {unpinState.status === AsyncStatus.Loading ? ( + + ) : ( + + )} + + )} + + ); + + if (pinnedEvent === undefined) return ; + if (pinnedEvent === null) + return ( + + + Failed to load message! + + {renderOptions()} + + ); + + const sender = pinnedEvent.getSender()!; + const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender; + const senderAvatarMxc = getMemberAvatarMxc(room, sender); + const getContent = (() => pinnedEvent.getContent()) as GetContentCallback; + return ( + + + } + /> + + + } + > + + + + + {displayName} + + + + {renderOptions()} + + {pinnedEvent.replyEventId && ( + + )} + {renderContent(pinnedEvent.getType(), false, pinnedEvent, displayName, getContent)} + + ); +} + +type RoomPinMenuProps = { + room: Room; + requestClose: () => void; +}; +export const RoomPinMenu = forwardRef( + ({ room, requestClose }, ref) => { + const mx = useMatrixClient(); + const userId = mx.getUserId()!; + const powerLevels = usePowerLevelsContext(); + const { canSendStateEvent, getPowerLevel } = usePowerLevelsAPI(powerLevels); + const canPinEvent = canSendStateEvent(StateEvent.RoomPinnedEvents, getPowerLevel(userId)); + + const pinnedEvents = useRoomPinnedEvents(room); + const sortedPinnedEvent = useMemo(() => Array.from(pinnedEvents).reverse(), [pinnedEvents]); + const useAuthentication = useMediaAuthentication(); + const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); + const [urlPreview] = useSetting(settingsAtom, 'urlPreview'); + const { navigateRoom } = useRoomNavigate(); + const scrollRef = useRef(null); + + const virtualizer = useVirtualizer({ + count: sortedPinnedEvent.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 75, + overscan: 4, + }); + + const mentionClickHandler = useMentionClickHandler(room.roomId); + const spoilerClickHandler = useSpoilerClickHandler(); + + const linkifyOpts = useMemo( + () => ({ + ...LINKIFY_OPTS, + render: factoryRenderLinkifyWithMention((href) => + renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler)) + ), + }), + [mx, room, mentionClickHandler] + ); + const htmlReactParserOptions = useMemo( + () => + getReactCustomHtmlParser(mx, room.roomId, { + linkifyOpts, + useAuthentication, + handleSpoilerClick: spoilerClickHandler, + handleMentionClick: mentionClickHandler, + }), + [mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication] + ); + + const renderMatrixEvent = useMatrixEventRenderer<[MatrixEvent, string, GetContentCallback]>( + { + [MessageEvent.RoomMessage]: (event, displayName, getContent) => { + if (event.isRedacted()) { + return ( + + ); + } + + return ( + + ); + }, + [MessageEvent.RoomMessageEncrypted]: (event, displayName) => { + const eventId = event.getId()!; + const evtTimeline = room.getTimelineForEvent(eventId); + + const mEvent = evtTimeline?.getEvents().find((e) => e.getId() === eventId); + + if (!mEvent || !evtTimeline) { + return ( + + + {event.getType()} + {' event'} + + + ); + } + + return ( + + {() => { + if (mEvent.isRedacted()) return ; + if (mEvent.getType() === MessageEvent.Sticker) + return ( + ( + } + renderViewer={(p) => } + /> + )} + /> + ); + if (mEvent.getType() === MessageEvent.RoomMessage) { + const editedEvent = getEditedEvent(eventId, mEvent, evtTimeline.getTimelineSet()); + const getContent = (() => + editedEvent?.getContent()['m.new_content'] ?? + mEvent.getContent()) as GetContentCallback; + + return ( + + ); + } + if (mEvent.getType() === MessageEvent.RoomMessageEncrypted) + return ( + + + + ); + return ( + + + + ); + }} + + ); + }, + [MessageEvent.Sticker]: (event, displayName, getContent) => { + if (event.isRedacted()) { + return ( + + ); + } + return ( + ( + } + renderViewer={(p) => } + /> + )} + /> + ); + }, + }, + undefined, + (event) => { + if (event.isRedacted()) { + return ; + } + return ( + + + {event.getType()} + {' event'} + + + ); + } + ); + + const handleOpen = (roomId: string, eventId: string) => { + navigateRoom(roomId, eventId); + requestClose(); + }; + + return ( + + +
+ + Pinned Messages + + + + + + +
+ + + + {sortedPinnedEvent.length > 0 ? ( +
+ {virtualizer.getVirtualItems().map((vItem) => { + const eventId = sortedPinnedEvent[vItem.index]; + if (!eventId) return null; + + return ( + + + + + + ); + })} +
+ ) : ( + + + + + No Pinned Messages + + + Users with sufficient power level can pin a messages from its context menu. + + + + )} +
+
+
+
+
+ ); + } +); diff --git a/src/app/features/room/room-pin-menu/index.ts b/src/app/features/room/room-pin-menu/index.ts new file mode 100644 index 00000000..65ddaeea --- /dev/null +++ b/src/app/features/room/room-pin-menu/index.ts @@ -0,0 +1 @@ +export * from './RoomPinMenu'; diff --git a/src/app/hooks/useRoomEvent.ts b/src/app/hooks/useRoomEvent.ts new file mode 100644 index 00000000..3ca2449f --- /dev/null +++ b/src/app/hooks/useRoomEvent.ts @@ -0,0 +1,56 @@ +import { MatrixEvent, Room } from 'matrix-js-sdk'; +import { useCallback, useMemo } from 'react'; +import to from 'await-to-js'; +import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; +import { useQuery } from '@tanstack/react-query'; +import { useMatrixClient } from './useMatrixClient'; + +const useFetchEvent = (room: Room, eventId: string) => { + const mx = useMatrixClient(); + + const fetchEventCallback = useCallback(async () => { + const evt = await mx.fetchRoomEvent(room.roomId, eventId); + const mEvent = new MatrixEvent(evt); + + if (mEvent.isEncrypted() && mx.getCrypto()) { + await to(mEvent.attemptDecryption(mx.getCrypto() as CryptoBackend)); + } + + return mEvent; + }, [mx, room.roomId, eventId]); + + return fetchEventCallback; +}; + +/** + * + * @param room + * @param eventId + * @returns `MatrixEvent`, `undefined` means loading, `null` means failure + */ +export const useRoomEvent = ( + room: Room, + eventId: string, + getLocally?: () => MatrixEvent | undefined +) => { + const event = useMemo(() => { + if (getLocally) return getLocally(); + return room.findEventById(eventId); + }, [room, eventId, getLocally]); + + const fetchEvent = useFetchEvent(room, eventId); + + const { data, error } = useQuery({ + enabled: event === undefined, + queryKey: [room.roomId, eventId], + queryFn: fetchEvent, + staleTime: Infinity, + gcTime: 60 * 60 * 1000, // 1hour + }); + + if (event) return event; + if (data) return data; + if (error) return null; + + return undefined; +}; diff --git a/src/app/hooks/useRoomPinnedEvents.ts b/src/app/hooks/useRoomPinnedEvents.ts new file mode 100644 index 00000000..9ab1d6bd --- /dev/null +++ b/src/app/hooks/useRoomPinnedEvents.ts @@ -0,0 +1,15 @@ +import { useMemo } from 'react'; +import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types'; +import { Room } from 'matrix-js-sdk'; +import { StateEvent } from '../../types/matrix/room'; +import { useStateEvent } from './useStateEvent'; + +export const useRoomPinnedEvents = (room: Room): string[] => { + const pinEvent = useStateEvent(room, StateEvent.RoomPinnedEvents); + const events = useMemo(() => { + const content = pinEvent?.getContent(); + return content?.pinned ?? []; + }, [pinEvent]); + + return events; +}; diff --git a/src/app/pages/client/inbox/Notifications.tsx b/src/app/pages/client/inbox/Notifications.tsx index 64eabc99..0c832b09 100644 --- a/src/app/pages/client/inbox/Notifications.tsx +++ b/src/app/pages/client/inbox/Notifications.tsx @@ -427,7 +427,14 @@ function RoomNotificationsGroupComp({ userId={event.sender} src={ senderAvatarMxc - ? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined + ? mxcUrlToHttp( + mx, + senderAvatarMxc, + useAuthentication, + 48, + 48, + 'crop' + ) ?? undefined : undefined } alt={displayName} @@ -459,7 +466,6 @@ function RoomNotificationsGroupComp({
{replyEventId && (