Compare commits

..

38 Commits

Author SHA1 Message Date
Krishan 02106a99b9 Release v4.10.4 (#2688) 2026-02-23 22:32:06 +11:00
dependabot[bot] df3a3ba789 Bump docker/setup-buildx-action from 3.11.1 to 3.12.0 (#2641)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.11.1 to 3.12.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.11.1...v3.12.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 3.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 18:18:01 +11:00
dependabot[bot] cd80d4c9e8 Bump docker/build-push-action from 6.18.0 to 6.19.2 (#2642)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.18.0 to 6.19.2.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.18.0...v6.19.2)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.19.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 18:13:17 +11:00
Krishan dab44edef2 Add prod-deploy.yml to Docker PR workflow paths (#2687) 2026-02-23 18:12:37 +11:00
Ajay Bura ed0ad61bc4 Verify SSO window message origin (#2686) 2026-02-23 18:08:25 +11:00
Ajay Bura b2cb717178 fix noreferrer typo in url preview link (#2685) 2026-02-23 17:56:14 +11:00
dependabot[bot] 7a9f6d2223 Bump linkifyjs and linkify-react from 4.1.3 to 4.3.2 (#2682)
* Bump linkifyjs from 4.1.3 to 4.3.2

Bumps [linkifyjs](https://github.com/nfrasser/linkifyjs/tree/HEAD/packages/linkifyjs) from 4.1.3 to 4.3.2.
- [Release notes](https://github.com/nfrasser/linkifyjs/releases)
- [Changelog](https://github.com/nfrasser/linkifyjs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nfrasser/linkifyjs/commits/v4.3.2/packages/linkifyjs)

---
updated-dependencies:
- dependency-name: linkifyjs
  dependency-version: 4.3.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* update linkify react

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com>
2026-02-23 17:43:15 +11:00
Ajay Bura a9022184fc Set message power to moderator in space (#2684) 2026-02-23 16:57:39 +11:00
renovate[bot] 826b3c2997 chore(deps): update actions/setup-node action to v6 (#2681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-22 18:57:23 +11:00
dependabot[bot] 2e6c5f7c04 Bump actions/upload-artifact from 4.6.2 to 6.0.0 (#2644)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 6.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.2...v6.0.0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-22 18:26:54 +11:00
dependabot[bot] 2d6730de56 Bump actions/checkout from 4.2.0 to 6.0.2 (#2640)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.0 to 6.0.2.
- [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.2.0...v6.0.2)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-22 18:26:08 +11:00
Krishan b6cc0e3077 Update node to v24.13.1 LTS (#2622)
* Update node to v24.13.1 LTS

* Fix dockerfile node version

* Simplify node and nginx version, bump nginx

* Fix casing
2026-02-22 18:15:23 +11:00
Ajay Bura 91c8731940 Add permission for managing emojis & stickers (#2678)
add permission for managing emojis & stickers
2026-02-22 15:48:23 +11:00
renovate[bot] 1f03891b25 fix(deps): update dependency folds to v2.6.1 (#2679)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-22 15:43:30 +11:00
Ajay Bura 9ff15b8b03 fix space lobby / search selected hook not working (#2675) 2026-02-22 15:14:04 +11:00
Ajay Bura 170f5cd473 Request session info from sw if missing (#2664)
* request session info from sw if missing

* fix async session request in fetch

* respond fetch synchronously and add early check for non media requests  (#2670)

* make sure we call respondWith synchronously

* simplify isMediaRequest in sw

* improve naming in sw

* get back baseUrl check into validMediaRequest

* pass original request into fetch in sw

* extract mediaPath util and performs checks properly

---------

Co-authored-by: mmmykhailo <35040944+mmmykhailo@users.noreply.github.com>
2026-02-21 17:51:27 +11:00
Krishan 29ec172c8b Release v4.10.3 (#2608) 2026-02-16 22:19:21 +11:00
Rin 0f220f50d6 fix: add noreferrer to sanitized links for improved privacy consistency (#2628)
Enhance privacy by adding noreferrer to sanitized links
2026-02-16 19:54:05 +11:00
Ajay Bura d866c1b903 fix room back button not working after router update (#2630) 2026-02-16 19:51:55 +11:00
Ajay Bura fbde1a2030 fix: image not loading on mobile after lock/unlock (#2631)
image not loading on mobile after lock/unlock
2026-02-16 19:51:09 +11:00
Krishan 4ba7b9162d Revert "fix: set m.fully_read marker when marking rooms as read" (#2629)
Revert "Set m.fully_read marker when marking rooms as read (#2587)"

This reverts commit 9d49418a1f.
2026-02-16 06:03:37 +11:00
Andrew Murphy 9d49418a1f Set m.fully_read marker when marking rooms as read (#2587)
Previously markAsRead() only sent m.read receipts via sendReadReceipt().
This meant the read position was not persisted across page refreshes,
especially noticeable in bridged rooms.

Now uses setRoomReadMarkers() which sets both:
- m.fully_read marker (persistent read position)
- m.read receipt

Fixes issue where rooms would still show as unread after refresh.
2026-02-14 17:32:10 +11:00
Ajay Bura 3522751a15 Prevent invalid mxc from getting used (#2609) 2026-02-14 17:12:28 +11:00
Ajay Bura 074c555294 Post session info to service worker instead of asking from sw (#2605)
post session info to service worker instead of asking from sw on each request
2026-02-14 17:11:36 +11:00
renovate[bot] 206a927f30 fix(deps): update dependency react-router-dom to v6.30.3 (#2612)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 17:10:43 +11:00
Andrew Murphy fd37dfe3f9 Fix muted rooms showing unread badges (#2581)
fix: detect muted rooms with empty actions array

The mute detection was checking for `actions[0] === "dont_notify"` but
Cinny sets `actions: []` (empty array) when muting a room, which is
the correct behavior per Matrix spec where empty actions means no
notification.

This caused muted rooms to still show unread badges and contribute to
space badge counts.

Fixes the isMutedRule check to handle both:
- Empty actions array (current Matrix spec)
- "dont_notify" string (deprecated but may exist in older rules)
2026-02-12 21:45:37 +11:00
Gimle Larpes 1ce6ca2b07 Re-add mEvent.getSender() === mx.getUserId() check for deletion of messages (#2607)
* hide "Delete Message" if it is forbidden

* Fix the stuff I broke :/
2026-02-12 21:40:11 +11:00
renovate[bot] 83e5125b37 fix(deps): update dependency folds to v2.5.0 (#2606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-12 16:56:47 +11:00
Gimle Larpes ca82aa283a Hide "Delete Message" if it is forbidden (#2602)
hide "Delete Message" if it is forbidden
2026-02-12 16:27:17 +11:00
Zach 8ce33ee6ff Replace envs.net with unredacted.org in config (#2601)
* Replace 'envs.net' with 'unredacted.org' in config

https://envs.net/ is shutting down their Matrix server

* Update defaultHomeserver and reorder servers list

* Remove 'monero.social' from homeserver list
2026-02-12 10:39:58 +11:00
Santhoshkumar044 073a9f5786 Fix room alias mention triggering room-wide notifications (#2562)
* fix: prevent room alias mentions from triggering @room notifications

* fix: Simplify room mention to exact match on @room
2026-01-12 23:21:00 +11:00
dependabot[bot] 655c1c9aff Bump docker/login-action from 3.5.0 to 3.6.0 (#2496)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3.5.0...v3.6.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 16:30:39 +11:00
dependabot[bot] 17d4bceb42 Bump nginx from 1.29.1-alpine to 1.29.3-alpine (#2525)
Bumps nginx from 1.29.1-alpine to 1.29.3-alpine.

---
updated-dependencies:
- dependency-name: nginx
  dependency-version: 1.29.3-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 16:16:20 +11:00
willow 0f61f2f328 Fix typo: change "Advance Options" to "Advanced Options" (#2537) 2025-11-27 16:01:40 +11:00
Krishan c88cb4bca9 Release v4.10.2 (#2528) 2025-11-05 17:49:56 +11:00
Ajay Bura 46c02b89de Update folds to fix broken scrollbar color (#2505) 2025-10-15 17:30:03 +11:00
Ajay Bura e13d97aa98 Fix member are not sorted correctly after last js-sdk update (#2504) 2025-10-15 17:27:11 +11:00
Krishan 958ae8945d Release v4.10.1 (#2495) 2025-09-29 14:34:38 +10:00
67 changed files with 408 additions and 1383 deletions
+5 -5
View File
@@ -12,11 +12,11 @@ jobs:
PR_NUMBER: ${{github.event.number}} PR_NUMBER: ${{github.event.number}}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v6.0.2
- name: Setup node - name: Setup node
uses: actions/setup-node@v4.4.0 uses: actions/setup-node@v6.2.0
with: with:
node-version: 20.12.2 node-version: 24.13.1
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@@ -25,7 +25,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096' NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build run: npm run build
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4.6.2 uses: actions/upload-artifact@v6.0.0
with: with:
name: preview name: preview
path: dist path: dist
@@ -33,7 +33,7 @@ jobs:
- name: Save pr number - name: Save pr number
run: echo ${PR_NUMBER} > ./pr.txt run: echo ${PR_NUMBER} > ./pr.txt
- name: Upload pr number - name: Upload pr number
uses: actions/upload-artifact@v4.6.2 uses: actions/upload-artifact@v6.0.0
with: with:
name: pr name: pr
path: ./pr.txt path: ./pr.txt
+3 -2
View File
@@ -5,15 +5,16 @@ on:
paths: paths:
- 'Dockerfile' - 'Dockerfile'
- '.github/workflows/docker-pr.yml' - '.github/workflows/docker-pr.yml'
- '.github/workflows/prod-deploy.yml'
jobs: jobs:
docker-build: docker-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v6.0.2
- name: Build Docker image - name: Build Docker image
uses: docker/build-push-action@v6.18.0 uses: docker/build-push-action@v6.19.2
with: with:
context: . context: .
push: false push: false
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.2.0 uses: actions/checkout@v6.0.2
- name: NPM Lockfile Changes - name: NPM Lockfile Changes
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891 uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
with: with:
+3 -3
View File
@@ -11,11 +11,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v6.0.2
- name: Setup node - name: Setup node
uses: actions/setup-node@v4.4.0 uses: actions/setup-node@v6.2.0
with: with:
node-version: 20.12.2 node-version: 24.13.1
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
+8 -8
View File
@@ -10,11 +10,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v6.0.2
- name: Setup node - name: Setup node
uses: actions/setup-node@v4.4.0 uses: actions/setup-node@v6.2.0
with: with:
node-version: 20.12.2 node-version: 24.13.1
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@@ -66,18 +66,18 @@ jobs:
packages: write packages: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v6.0.2
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0 uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1 uses: docker/setup-buildx-action@v3.12.0
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3.5.0 uses: docker/login-action@v3.6.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to the Container registry - name: Login to the Container registry
uses: docker/login-action@v3.5.0 uses: docker/login-action@v3.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -90,7 +90,7 @@ jobs:
${{ secrets.DOCKER_USERNAME }}/cinny ${{ secrets.DOCKER_USERNAME }}/cinny
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6.18.0 uses: docker/build-push-action@v6.19.2
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
+2 -2
View File
@@ -1,5 +1,5 @@
## Builder ## Builder
FROM node:20.12.2-alpine3.18 as builder FROM node:24.13.1-alpine AS builder
WORKDIR /src WORKDIR /src
@@ -11,7 +11,7 @@ RUN npm run build
## App ## App
FROM nginx:1.29.1-alpine FROM nginx:1.29.5-alpine
COPY --from=builder /src/dist /app COPY --from=builder /src/dist /app
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
+1 -1
View File
@@ -83,7 +83,7 @@ mxFo+ioe/ABCufSmyqFye0psX3Sp
## Local development ## Local development
> [!TIP] > [!TIP]
> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Iron LTS (v20). > We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Krypton LTS (v24.13.1).
Execute the following commands to start a development server: Execute the following commands to start a development server:
```sh ```sh
+4 -5
View File
@@ -1,11 +1,10 @@
{ {
"defaultHomeserver": 2, "defaultHomeserver": 1,
"homeserverList": [ "homeserverList": [
"converser.eu", "converser.eu",
"envs.net",
"matrix.org", "matrix.org",
"monero.social",
"mozilla.org", "mozilla.org",
"unredacted.org",
"xmr.se" "xmr.se"
], ],
"allowCustomHomeservers": true, "allowCustomHomeservers": true,
@@ -15,7 +14,7 @@
"spaces": [ "spaces": [
"#cinny-space:matrix.org", "#cinny-space:matrix.org",
"#community:matrix.org", "#community:matrix.org",
"#space:envs.net", "#space:unredacted.org",
"#science-space:matrix.org", "#science-space:matrix.org",
"#libregaming-games:tchncs.de", "#libregaming-games:tchncs.de",
"#mathematics-on:matrix.org" "#mathematics-on:matrix.org"
@@ -28,7 +27,7 @@
"#PrivSec.dev:arcticfoxes.net", "#PrivSec.dev:arcticfoxes.net",
"#disroot:aria-net.org" "#disroot:aria-net.org"
], ],
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"] "servers": [ "matrix.org", "mozilla.org", "unredacted.org" ]
}, },
"hashRouter": { "hashRouter": {
+32 -27
View File
@@ -1,12 +1,12 @@
{ {
"name": "cinny", "name": "cinny",
"version": "4.10.0", "version": "4.10.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cinny", "name": "cinny",
"version": "4.10.0", "version": "4.10.4",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.1.6", "@atlaskit/pragmatic-drag-and-drop": "1.1.6",
@@ -32,7 +32,7 @@
"emojibase-data": "15.3.2", "emojibase-data": "15.3.2",
"file-saver": "2.0.5", "file-saver": "2.0.5",
"focus-trap-react": "10.0.2", "focus-trap-react": "10.0.2",
"folds": "2.5.0", "folds": "2.6.1",
"html-dom-parser": "4.0.0", "html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0", "html-react-parser": "4.2.0",
"i18next": "23.12.2", "i18next": "23.12.2",
@@ -41,8 +41,8 @@
"immer": "9.0.16", "immer": "9.0.16",
"is-hotkey": "0.2.0", "is-hotkey": "0.2.0",
"jotai": "2.6.0", "jotai": "2.6.0",
"linkify-react": "4.1.3", "linkify-react": "4.3.2",
"linkifyjs": "4.1.3", "linkifyjs": "4.3.2",
"matrix-js-sdk": "38.2.0", "matrix-js-sdk": "38.2.0",
"millify": "6.1.0", "millify": "6.1.0",
"pdfjs-dist": "4.2.67", "pdfjs-dist": "4.2.67",
@@ -56,7 +56,7 @@
"react-google-recaptcha": "2.1.0", "react-google-recaptcha": "2.1.0",
"react-i18next": "15.0.0", "react-i18next": "15.0.0",
"react-range": "1.8.14", "react-range": "1.8.14",
"react-router-dom": "6.20.0", "react-router-dom": "6.30.3",
"sanitize-html": "2.12.1", "sanitize-html": "2.12.1",
"slate": "0.112.0", "slate": "0.112.0",
"slate-dom": "0.112.2", "slate-dom": "0.112.2",
@@ -3699,9 +3699,10 @@
} }
}, },
"node_modules/@remix-run/router": { "node_modules/@remix-run/router": {
"version": "1.13.0", "version": "1.23.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.0.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
"integrity": "sha512-5dMOnVnefRsl4uRnAdoWjtVTdh8e6aZqgM4puy9nmEADH72ck+uXwzpJLEKE9Q6F8ZljNewLgmTfkxUrBdv4WA==", "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
"license": "MIT",
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
@@ -7157,9 +7158,9 @@
} }
}, },
"node_modules/folds": { "node_modules/folds": {
"version": "2.5.0", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/folds/-/folds-2.5.0.tgz", "resolved": "https://registry.npmjs.org/folds/-/folds-2.6.1.tgz",
"integrity": "sha512-UJhvXAQ1XnZ9w10KJwSW+frvzzWE/zcF0dH3fDVCD70RFHAxwEi0UkkVS8CaZGxZF2Wvt3qTJyTS5LW3LwwUAw==", "integrity": "sha512-0L1ZSqwjFSg2fesa//C4DgP47Vp/KqDuzjAaOEYN21AvoptyVI+6OEXWrtIdE8DPQCZYr0bV+tqbrLyA6uAhaw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peerDependencies": { "peerDependencies": {
"@vanilla-extract/css": "1.9.2", "@vanilla-extract/css": "1.9.2",
@@ -8491,18 +8492,20 @@
} }
}, },
"node_modules/linkify-react": { "node_modules/linkify-react": {
"version": "4.1.3", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz", "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.3.2.tgz",
"integrity": "sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA==", "integrity": "sha512-mi744h1hf+WDsr+paJgSBBgYNLMWNSHyM9V9LVUo03RidNGdw1VpI7Twnt+K3pEh3nIzB4xiiAgZxpd61ItKpQ==",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"linkifyjs": "^4.0.0", "linkifyjs": "^4.0.0",
"react": ">= 15.0.0" "react": ">= 15.0.0"
} }
}, },
"node_modules/linkifyjs": { "node_modules/linkifyjs": {
"version": "4.1.3", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz", "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
"integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==" "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
"license": "MIT"
}, },
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
@@ -9605,11 +9608,12 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "6.20.0", "version": "6.30.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.20.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
"integrity": "sha512-pVvzsSsgUxxtuNfTHC4IxjATs10UaAtvLGVSA1tbUE4GDaOSU1Esu2xF5nWLz7KPiMuW8BJWuPFdlGYJ7/rW0w==", "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@remix-run/router": "1.13.0" "@remix-run/router": "1.23.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -9619,12 +9623,13 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "6.20.0", "version": "6.30.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.20.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
"integrity": "sha512-CbcKjEyiSVpA6UtCHOIYLUYn/UJfwzp55va4yEfpk7JBN3GPqWfHrdLkAvNCcpXr8QoihcDMuk0dzWZxtlB/mQ==", "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
"license": "MIT",
"dependencies": { "dependencies": {
"@remix-run/router": "1.13.0", "@remix-run/router": "1.23.2",
"react-router": "6.20.0" "react-router": "6.30.3"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
+5 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "cinny", "name": "cinny",
"version": "4.10.0", "version": "4.10.4",
"description": "Yet another matrix client", "description": "Yet another matrix client",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@@ -43,7 +43,7 @@
"emojibase-data": "15.3.2", "emojibase-data": "15.3.2",
"file-saver": "2.0.5", "file-saver": "2.0.5",
"focus-trap-react": "10.0.2", "focus-trap-react": "10.0.2",
"folds": "2.5.0", "folds": "2.6.1",
"html-dom-parser": "4.0.0", "html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0", "html-react-parser": "4.2.0",
"i18next": "23.12.2", "i18next": "23.12.2",
@@ -52,8 +52,8 @@
"immer": "9.0.16", "immer": "9.0.16",
"is-hotkey": "0.2.0", "is-hotkey": "0.2.0",
"jotai": "2.6.0", "jotai": "2.6.0",
"linkify-react": "4.1.3", "linkify-react": "4.3.2",
"linkifyjs": "4.1.3", "linkifyjs": "4.3.2",
"matrix-js-sdk": "38.2.0", "matrix-js-sdk": "38.2.0",
"millify": "6.1.0", "millify": "6.1.0",
"pdfjs-dist": "4.2.67", "pdfjs-dist": "4.2.67",
@@ -67,7 +67,7 @@
"react-google-recaptcha": "2.1.0", "react-google-recaptcha": "2.1.0",
"react-i18next": "15.0.0", "react-i18next": "15.0.0",
"react-range": "1.8.14", "react-range": "1.8.14",
"react-router-dom": "6.20.0", "react-router-dom": "6.30.3",
"sanitize-html": "2.12.1", "sanitize-html": "2.12.1",
"slate": "0.112.0", "slate": "0.112.0",
"slate-dom": "0.112.2", "slate-dom": "0.112.2",
+6 -2
View File
@@ -51,8 +51,12 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
}, },
location.pathname location.pathname
); );
if (spaceMatch?.params.spaceIdOrAlias) { const encodedSpaceIdOrAlias = spaceMatch?.params.spaceIdOrAlias;
navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias)); const decodedSpaceIdOrAlias =
encodedSpaceIdOrAlias && decodeURIComponent(encodedSpaceIdOrAlias);
if (decodedSpaceIdOrAlias) {
navigate(getSpacePath(decodedSpaceIdOrAlias));
return; return;
} }
if ( if (
+8
View File
@@ -74,6 +74,10 @@ export const createRoomParentState = (parent: Room) => ({
}, },
}); });
const createSpacePowerLevelsOverride = () => ({
events_default: 50,
});
export const createRoomEncryptionState = () => ({ export const createRoomEncryptionState = () => ({
type: 'm.room.encryption', type: 'm.room.encryption',
state_key: '', state_key: '',
@@ -121,6 +125,10 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
initial_state: initialState, initial_state: initialState,
}; };
if (data.type === RoomType.Space) {
options.power_level_content_override = createSpacePowerLevelsOverride();
}
const result = await mx.createRoom(options); const result = await mx.createRoom(options);
if (data.parent) { if (data.parent) {
@@ -88,6 +88,8 @@ export function EmoticonAutocomplete({
{autoCompleteEmoticon.map((emoticon) => { {autoCompleteEmoticon.map((emoticon) => {
const isCustomEmoji = 'url' in emoticon; const isCustomEmoji = 'url' in emoticon;
const key = isCustomEmoji ? emoticon.url : emoticon.unicode; const key = isCustomEmoji ? emoticon.url : emoticon.unicode;
const customEmojiUrl = mxcUrlToHttp(mx, key, useAuthentication);
return ( return (
<MenuItem <MenuItem
key={emoticon.shortcode + key} key={emoticon.shortcode + key}
@@ -98,11 +100,11 @@ export function EmoticonAutocomplete({
} }
onClick={() => handleAutocomplete(key, emoticon.shortcode)} onClick={() => handleAutocomplete(key, emoticon.shortcode)}
before={ before={
isCustomEmoji ? ( isCustomEmoji && customEmojiUrl ? (
<Box <Box
shrink="No" shrink="No"
as="img" as="img"
src={mxcUrlToHttp(mx, key, useAuthentication) || key} src={customEmojiUrl}
alt={emoticon.shortcode} alt={emoticon.shortcode}
style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }} style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }}
/> />
+2 -1
View File
@@ -212,9 +212,10 @@ export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): M
if (node.type === BlockType.CodeBlock) return; if (node.type === BlockType.CodeBlock) return;
if (node.type === BlockType.Mention) { if (node.type === BlockType.Mention) {
if (node.id === getCanonicalAliasOrRoomId(mx, roomId)) { if (node.name === '@room') {
mentionData.room = true; mentionData.room = true;
} }
if (isUserId(node.id) && node.id !== mx.getUserId()) { if (isUserId(node.id) && node.id !== mx.getUserId()) {
mentionData.users.add(node.id); mentionData.users.add(node.id);
} }
@@ -202,8 +202,7 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
const url = const url =
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined;
pack.meta.avatar;
return ( return (
<ImageGroupIcon <ImageGroupIcon
@@ -266,7 +265,7 @@ function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSide
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
const url = const url =
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || pack.meta.avatar; mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined;
return ( return (
<ImageGroupIcon <ImageGroupIcon
@@ -68,7 +68,7 @@ export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiIte
loading="lazy" loading="lazy"
className={css.CustomEmojiImg} className={css.CustomEmojiImg}
alt={image.body || image.shortcode} alt={image.body || image.shortcode}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url} src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
/> />
</Box> </Box>
); );
@@ -98,7 +98,7 @@ export function StickerItem({ mx, useAuthentication, image }: StickerItemProps)
loading="lazy" loading="lazy"
className={css.StickerImg} className={css.StickerImg}
alt={image.body || image.shortcode} alt={image.body || image.shortcode}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url} src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
/> />
</Box> </Box>
); );
+2 -1
View File
@@ -27,7 +27,8 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow
const [downloadState, download] = useAsyncCallback( const [downloadState, download] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
const fileContent = encInfo const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl); : await downloadMedia(mediaUrl);
-6
View File
@@ -1,6 +0,0 @@
import { style } from '@vanilla-extract/css';
export const Time = style({
fontWeight: 'inherit',
flexShrink: 0,
});
+3 -13
View File
@@ -1,15 +1,12 @@
import React, { ComponentProps } from 'react'; import React, { ComponentProps } from 'react';
import { Text, as } from 'folds'; import { Text, as } from 'folds';
import classNames from 'classnames';
import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../utils/time'; import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../utils/time';
import * as css from './Time.css';
export type TimeProps = { export type TimeProps = {
compact?: boolean; compact?: boolean;
ts: number; ts: number;
hour24Clock: boolean; hour24Clock: boolean;
dateFormatString: string; dateFormatString: string;
inheritPriority?: boolean;
}; };
/** /**
@@ -25,7 +22,7 @@ export type TimeProps = {
* @returns {React.ReactElement} A <Text as="time"> element with the formatted date/time. * @returns {React.ReactElement} A <Text as="time"> element with the formatted date/time.
*/ */
export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>( export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
({ compact, hour24Clock, dateFormatString, ts, inheritPriority, className, ...props }, ref) => { ({ compact, hour24Clock, dateFormatString, ts, ...props }, ref) => {
const formattedTime = timeHourMinute(ts, hour24Clock); const formattedTime = timeHourMinute(ts, hour24Clock);
let time = ''; let time = '';
@@ -36,18 +33,11 @@ export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
} else if (yesterday(ts)) { } else if (yesterday(ts)) {
time = `Yesterday ${formattedTime}`; time = `Yesterday ${formattedTime}`;
} else { } else {
time = `${timeDayMonYear(ts, dateFormatString)}, ${formattedTime}`; time = `${timeDayMonYear(ts, dateFormatString)} ${formattedTime}`;
} }
return ( return (
<Text <Text as="time" style={{ flexShrink: 0 }} size="T200" priority="300" {...props} ref={ref}>
as="time"
className={classNames(css.Time, className)}
size="T200"
priority={inheritPriority ? undefined : '300'}
{...props}
ref={ref}
>
{time} {time}
</Text> </Text>
); );
@@ -54,7 +54,8 @@ export function AudioContent({
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
const fileContent = encInfo const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl); : await downloadMedia(mediaUrl);
@@ -86,7 +86,8 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
const [textState, loadText] = useAsyncCallback( const [textState, loadText] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
const fileContent = encInfo const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl); : await downloadMedia(mediaUrl);
@@ -176,7 +177,8 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
const [pdfState, loadPdf] = useAsyncCallback( const [pdfState, loadPdf] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
const fileContent = encInfo const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl); : await downloadMedia(mediaUrl);
@@ -253,7 +255,8 @@ export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFil
const [downloadState, download] = useAsyncCallback( const [downloadState, download] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
const fileContent = encInfo const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl); : await downloadMedia(mediaUrl);
@@ -87,7 +87,8 @@ export const ImageContent = as<'div', ImageContentProps>(
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
if (encInfo) { if (encInfo) {
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) => const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo) decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo)
@@ -23,7 +23,8 @@ export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) {
throw new Error('Failed to load thumbnail'); throw new Error('Failed to load thumbnail');
} }
const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? thumbMxcUrl; const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
if (encInfo) { if (encInfo) {
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) => const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo) decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo)
@@ -81,7 +81,8 @@ export const VideoContent = as<'div', VideoContentProps>(
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
const fileContent = encInfo const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => ? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
decryptFile(encBuf, mimeType, encInfo) decryptFile(encBuf, mimeType, encInfo)
+7 -2
View File
@@ -26,7 +26,12 @@ export function SSOStage({
useEffect(() => { useEffect(() => {
const handleMessage = (evt: MessageEvent) => { const handleMessage = (evt: MessageEvent) => {
if (ssoWindow && evt.data === 'authDone' && evt.source === ssoWindow) { if (
evt.origin === new URL(ssoRedirectURL).origin &&
ssoWindow &&
evt.data === 'authDone' &&
evt.source === ssoWindow
) {
ssoWindow.close(); ssoWindow.close();
setSSOWindow(undefined); setSSOWindow(undefined);
handleSubmit(); handleSubmit();
@@ -37,7 +42,7 @@ export function SSOStage({
return () => { return () => {
window.removeEventListener('message', handleMessage); window.removeEventListener('message', handleMessage);
}; };
}, [ssoWindow, handleSubmit]); }, [ssoWindow, handleSubmit, ssoRedirectURL]);
return ( return (
<Dialog> <Dialog>
@@ -30,7 +30,15 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
if (previewStatus.status === AsyncStatus.Error) return null; if (previewStatus.status === AsyncStatus.Error) return null;
const renderContent = (prev: IPreviewUrlResponse) => { const renderContent = (prev: IPreviewUrlResponse) => {
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication, 256, 256, 'scale', false); const imgUrl = mxcUrlToHttp(
mx,
prev['og:image'] || '',
useAuthentication,
256,
256,
'scale',
false
);
return ( return (
<> <>
@@ -42,7 +50,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
as="a" as="a"
href={url} href={url}
target="_blank" target="_blank"
rel="no-referrer" rel="noreferrer"
size="T200" size="T200"
priority="300" priority="300"
> >
@@ -27,7 +27,7 @@ import { Page, PageContent, PageHeader } from '../../../components/page';
import { useRoom } from '../../../hooks/useRoom'; import { useRoom } from '../../../hooks/useRoom';
import { useRoomMembers } from '../../../hooks/useRoomMembers'; import { useRoomMembers } from '../../../hooks/useRoomMembers';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { usePowerLevels } from '../../../hooks/usePowerLevels'; import { useGetMemberPowerLevel, usePowerLevels } from '../../../hooks/usePowerLevels';
import { VirtualTile } from '../../../components/virtualizer'; import { VirtualTile } from '../../../components/virtualizer';
import { MemberTile } from '../../../components/member-tile'; import { MemberTile } from '../../../components/member-tile';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
@@ -87,12 +87,13 @@ export function Members({ requestClose }: MembersProps) {
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room); const creators = useRoomCreators(room);
const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels); const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
const getPowerLevel = useGetMemberPowerLevel(powerLevels);
const [membershipFilterIndex, setMembershipFilterIndex] = useState(0); const [membershipFilterIndex, setMembershipFilterIndex] = useState(0);
const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex'); const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex');
const membershipFilter = useMembershipFilter(membershipFilterIndex, useMembershipFilterMenu()); const membershipFilter = useMembershipFilter(membershipFilterIndex, useMembershipFilterMenu());
const memberSort = useMemberSort(sortFilterIndex, useMemberSortMenu()); const memberSort = useMemberSort(sortFilterIndex, useMemberSortMenu());
const memberPowerSort = useMemberPowerSort(creators); const memberPowerSort = useMemberPowerSort(creators, getPowerLevel);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
+1 -1
View File
@@ -183,7 +183,7 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
onClick={() => setAdvance(!advance)} onClick={() => setAdvance(!advance)}
type="button" type="button"
> >
<Text size="T200">Advance Options</Text> <Text size="T200">Advanced Options</Text>
</Chip> </Chip>
</Box> </Box>
</Box> </Box>
@@ -184,7 +184,7 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
onClick={() => setAdvance(!advance)} onClick={() => setAdvance(!advance)}
type="button" type="button"
> >
<Text size="T200">Advance Options</Text> <Text size="T200">Advanced Options</Text>
</Chip> </Chip>
</Box> </Box>
</Box> </Box>
@@ -59,7 +59,7 @@ export function General({ requestClose }: GeneralProps) {
<RoomLocalAddresses permissions={permissions} /> <RoomLocalAddresses permissions={permissions} />
</Box> </Box>
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
<Text size="L400">Advance Options</Text> <Text size="L400">Advanced Options</Text>
<RoomUpgrade permissions={permissions} requestClose={requestClose} /> <RoomUpgrade permissions={permissions} requestClose={requestClose} />
</Box> </Box>
</Box> </Box>
@@ -177,6 +177,13 @@ export const usePermissionGroups = (): PermissionGroup[] => {
const otherSettingsGroup: PermissionGroup = { const otherSettingsGroup: PermissionGroup = {
name: 'Other', name: 'Other',
items: [ items: [
{
location: {
state: true,
key: StateEvent.PoniesRoomEmotes,
},
name: 'Manage Emojis & Stickers',
},
{ {
location: { location: {
state: true, state: true,
+3 -2
View File
@@ -51,7 +51,7 @@ import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useMembershipFilter, useMembershipFilterMenu } from '../../hooks/useMemberFilter'; import { useMembershipFilter, useMembershipFilterMenu } from '../../hooks/useMemberFilter';
import { useMemberPowerSort, useMemberSort, useMemberSortMenu } from '../../hooks/useMemberSort'; import { useMemberPowerSort, useMemberSort, useMemberSortMenu } from '../../hooks/useMemberSort';
import { usePowerLevelsContext } from '../../hooks/usePowerLevels'; import { useGetMemberPowerLevel, usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { MembershipFilterMenu } from '../../components/MembershipFilterMenu'; import { MembershipFilterMenu } from '../../components/MembershipFilterMenu';
import { MemberSortMenu } from '../../components/MemberSortMenu'; import { MemberSortMenu } from '../../components/MemberSortMenu';
import { useOpenUserRoomProfile, useUserRoomProfileState } from '../../state/hooks/userRoomProfile'; import { useOpenUserRoomProfile, useUserRoomProfileState } from '../../state/hooks/userRoomProfile';
@@ -185,6 +185,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
const powerLevels = usePowerLevelsContext(); const powerLevels = usePowerLevelsContext();
const creators = useRoomCreators(room); const creators = useRoomCreators(room);
const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels); const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
const getPowerLevel = useGetMemberPowerLevel(powerLevels);
const fetchingMembers = members.length < room.getJoinedMemberCount(); const fetchingMembers = members.length < room.getJoinedMemberCount();
const openUserRoomProfile = useOpenUserRoomProfile(); const openUserRoomProfile = useOpenUserRoomProfile();
@@ -198,7 +199,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
const membershipFilter = useMembershipFilter(membershipFilterIndex, membershipFilterMenu); const membershipFilter = useMembershipFilter(membershipFilterIndex, membershipFilterMenu);
const memberSort = useMemberSort(sortFilterIndex, sortFilterMenu); const memberSort = useMemberSort(sortFilterIndex, sortFilterMenu);
const memberPowerSort = useMemberPowerSort(creators); const memberPowerSort = useMemberPowerSort(creators, getPowerLevel);
const typingMembers = useRoomTypingMember(room.roomId); const typingMembers = useRoomTypingMember(room.roomId);
+1 -13
View File
@@ -13,8 +13,6 @@ import { useKeyDown } from '../../hooks/useKeyDown';
import { markAsRead } from '../../utils/notifications'; import { markAsRead } from '../../utils/notifications';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomMembers } from '../../hooks/useRoomMembers'; import { useRoomMembers } from '../../hooks/useRoomMembers';
import { ThreadView } from './thread-view';
import { useActiveThread } from '../../state/hooks/roomToActiveThread';
export function Room() { export function Room() {
const { eventId } = useParams(); const { eventId } = useParams();
@@ -27,8 +25,6 @@ export function Room() {
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const members = useRoomMembers(mx, room.roomId); const members = useRoomMembers(mx, room.roomId);
const threadId = useActiveThread(room.roomId);
useKeyDown( useKeyDown(
window, window,
useCallback( useCallback(
@@ -45,19 +41,11 @@ export function Room() {
<PowerLevelsContextProvider value={powerLevels}> <PowerLevelsContextProvider value={powerLevels}>
<Box grow="Yes"> <Box grow="Yes">
<RoomView room={room} eventId={eventId} /> <RoomView room={room} eventId={eventId} />
{threadId ? ( {screenSize === ScreenSize.Desktop && isDrawer && (
<>
<Line variant="Surface" direction="Vertical" size="300" />
<ThreadView threadId={threadId} />
</>
) : (
screenSize === ScreenSize.Desktop &&
isDrawer && (
<> <>
<Line variant="Background" direction="Vertical" size="300" /> <Line variant="Background" direction="Vertical" size="300" />
<MembersDrawer key={room.roomId} room={room} members={members} /> <MembersDrawer key={room.roomId} room={room} members={members} />
</> </>
)
)} )}
</Box> </Box>
</PowerLevelsContextProvider> </PowerLevelsContextProvider>
+77 -41
View File
@@ -77,7 +77,6 @@ import {
decryptAllTimelineEvent, decryptAllTimelineEvent,
getEditedEvent, getEditedEvent,
getEventReactions, getEventReactions,
getEventThreadDetail,
getLatestEditableEvt, getLatestEditableEvt,
getMemberDisplayName, getMemberDisplayName,
getReactionContent, getReactionContent,
@@ -127,18 +126,6 @@ import { useAccessiblePowerTagColors, useGetMemberPowerTag } from '../../hooks/u
import { useTheme } from '../../hooks/useTheme'; import { useTheme } from '../../hooks/useTheme';
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag'; import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags'; import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
import { ThreadSelector, ThreadSelectorContainer } from './message/thread-selector';
import {
getEventIdAbsoluteIndex,
getFirstLinkedTimeline,
getLinkedTimelines,
getTimelineAndBaseIndex,
getTimelineEvent,
getTimelineRelativeIndex,
getTimelinesEventsCount,
timelineToEventsCount,
} from './utils';
import { useThreadSelector } from '../../state/hooks/roomToActiveThread';
const TimelineFloat = as<'div', css.TimelineFloatVariants>( const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => ( ({ position, className, ...props }, ref) => (
@@ -163,6 +150,79 @@ const TimelineDivider = as<'div', { variant?: ContainerColor | 'Inherit' }>(
) )
); );
export const getLiveTimeline = (room: Room): EventTimeline =>
room.getUnfilteredTimelineSet().getLiveTimeline();
export const getEventTimeline = (room: Room, eventId: string): EventTimeline | undefined => {
const timelineSet = room.getUnfilteredTimelineSet();
return timelineSet.getTimelineForEvent(eventId) ?? undefined;
};
export const getFirstLinkedTimeline = (
timeline: EventTimeline,
direction: Direction
): EventTimeline => {
const linkedTm = timeline.getNeighbouringTimeline(direction);
if (!linkedTm) return timeline;
return getFirstLinkedTimeline(linkedTm, direction);
};
export const getLinkedTimelines = (timeline: EventTimeline): EventTimeline[] => {
const firstTimeline = getFirstLinkedTimeline(timeline, Direction.Backward);
const timelines: EventTimeline[] = [];
for (
let nextTimeline: EventTimeline | null = firstTimeline;
nextTimeline;
nextTimeline = nextTimeline.getNeighbouringTimeline(Direction.Forward)
) {
timelines.push(nextTimeline);
}
return timelines;
};
export const timelineToEventsCount = (t: EventTimeline) => t.getEvents().length;
export const getTimelinesEventsCount = (timelines: EventTimeline[]): number => {
const timelineEventCountReducer = (count: number, tm: EventTimeline) =>
count + timelineToEventsCount(tm);
return timelines.reduce(timelineEventCountReducer, 0);
};
export const getTimelineAndBaseIndex = (
timelines: EventTimeline[],
index: number
): [EventTimeline | undefined, number] => {
let uptoTimelineLen = 0;
const timeline = timelines.find((t) => {
uptoTimelineLen += t.getEvents().length;
if (index < uptoTimelineLen) return true;
return false;
});
if (!timeline) return [undefined, 0];
return [timeline, uptoTimelineLen - timeline.getEvents().length];
};
export const getTimelineRelativeIndex = (absoluteIndex: number, timelineBaseIndex: number) =>
absoluteIndex - timelineBaseIndex;
export const getTimelineEvent = (timeline: EventTimeline, index: number): MatrixEvent | undefined =>
timeline.getEvents()[index];
export const getEventIdAbsoluteIndex = (
timelines: EventTimeline[],
eventTimeline: EventTimeline,
eventId: string
): number | undefined => {
const timelineIndex = timelines.findIndex((t) => t === eventTimeline);
if (timelineIndex === -1) return undefined;
const eventIndex = eventTimeline.getEvents().findIndex((evt) => evt.getId() === eventId);
if (eventIndex === -1) return undefined;
const baseIndex = timelines
.slice(0, timelineIndex)
.reduce((accValue, timeline) => timeline.getEvents().length + accValue, 0);
return baseIndex + eventIndex;
};
type RoomTimelineProps = { type RoomTimelineProps = {
room: Room; room: Room;
eventId?: string; eventId?: string;
@@ -172,14 +232,6 @@ type RoomTimelineProps = {
const PAGINATION_LIMIT = 80; const PAGINATION_LIMIT = 80;
export const getLiveTimeline = (room: Room): EventTimeline =>
room.getUnfilteredTimelineSet().getLiveTimeline();
export const getEventTimeline = (room: Room, eventId: string): EventTimeline | undefined => {
const timelineSet = room.getUnfilteredTimelineSet();
return timelineSet.getTimelineForEvent(eventId) ?? undefined;
};
type Timeline = { type Timeline = {
linkedTimelines: EventTimeline[]; linkedTimelines: EventTimeline[];
range: ItemRange; range: ItemRange;
@@ -419,6 +471,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const permissions = useRoomPermissions(creators, powerLevels); const permissions = useRoomPermissions(creators, powerLevels);
const canRedact = permissions.action('redact', mx.getSafeUserId()); const canRedact = permissions.action('redact', mx.getSafeUserId());
const canDeleteOwn = permissions.event(MessageEvent.RoomRedaction, mx.getSafeUserId());
const canSendReaction = permissions.event(MessageEvent.Reaction, mx.getSafeUserId()); const canSendReaction = permissions.event(MessageEvent.Reaction, mx.getSafeUserId());
const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, mx.getSafeUserId()); const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, mx.getSafeUserId());
const [editId, setEditId] = useState<string>(); const [editId, setEditId] = useState<string>();
@@ -430,7 +483,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const spoilerClickHandler = useSpoilerClickHandler(); const spoilerClickHandler = useSpoilerClickHandler();
const openUserRoomProfile = useOpenUserRoomProfile(); const openUserRoomProfile = useOpenUserRoomProfile();
const space = useSpaceOptionally(); const space = useSpaceOptionally();
const handleThreadClick = useThreadSelector(room.roomId);
const imagePackRooms: Room[] = useImagePackRooms(room.roomId, roomToParents); const imagePackRooms: Room[] = useImagePackRooms(room.roomId, roomToParents);
@@ -963,7 +1015,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
}, },
[editor] [editor]
); );
const { t } = useTranslation(); const { t } = useTranslation();
const renderMatrixEvent = useMatrixEventRenderer< const renderMatrixEvent = useMatrixEventRenderer<
@@ -984,7 +1035,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const senderId = mEvent.getSender() ?? ''; const senderId = mEvent.getSender() ?? '';
const senderDisplayName = const senderDisplayName =
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId; getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
const threadDetail = getEventThreadDetail(mEvent);
return ( return (
<Message <Message
@@ -998,7 +1048,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
collapse={collapse} collapse={collapse}
highlight={highlighted} highlight={highlighted}
edit={editId === mEventId} edit={editId === mEventId}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()} canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
canSendReaction={canSendReaction} canSendReaction={canSendReaction}
canPinEvent={canPinEvent} canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms} imagePackRooms={imagePackRooms}
@@ -1058,20 +1108,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
outlineAttachment={messageLayout === MessageLayout.Bubble} outlineAttachment={messageLayout === MessageLayout.Bubble}
/> />
)} )}
{threadDetail && (
<ThreadSelectorContainer>
<ThreadSelector
room={room}
threadId={mEventId}
threadDetail={threadDetail}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
outlined={messageLayout === MessageLayout.Bubble}
onClick={handleThreadClick}
/>
</ThreadSelectorContainer>
)}
</Message> </Message>
); );
}, },
@@ -1094,7 +1130,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
collapse={collapse} collapse={collapse}
highlight={highlighted} highlight={highlighted}
edit={editId === mEventId} edit={editId === mEventId}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()} canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
canSendReaction={canSendReaction} canSendReaction={canSendReaction}
canPinEvent={canPinEvent} canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms} imagePackRooms={imagePackRooms}
@@ -1212,7 +1248,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
messageLayout={messageLayout} messageLayout={messageLayout}
collapse={collapse} collapse={collapse}
highlight={highlighted} highlight={highlighted}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()} canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
canSendReaction={canSendReaction} canSendReaction={canSendReaction}
canPinEvent={canPinEvent} canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms} imagePackRooms={imagePackRooms}
-46
View File
@@ -69,7 +69,6 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useRoomCreators } from '../../hooks/useRoomCreators'; import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions'; import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { InviteUserPrompt } from '../../components/invite-user-prompt'; import { InviteUserPrompt } from '../../components/invite-user-prompt';
import { ThreadsMenu } from './threads-menu';
type RoomMenuProps = { type RoomMenuProps = {
room: Room; room: Room;
@@ -264,7 +263,6 @@ export function RoomViewHeader() {
const space = useSpaceOptionally(); const space = useSpaceOptionally();
const [menuAnchor, setMenuAnchor] = useState<RectCords>(); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>(); const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>();
const [threadsMenuAnchor, setThreadsMenuAnchor] = useState<RectCords>();
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
const pinnedEvents = useRoomPinnedEvents(room); const pinnedEvents = useRoomPinnedEvents(room);
@@ -297,10 +295,6 @@ export function RoomViewHeader() {
setPinMenuAnchor(evt.currentTarget.getBoundingClientRect()); setPinMenuAnchor(evt.currentTarget.getBoundingClientRect());
}; };
const handleOpenThreadsMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setThreadsMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
return ( return (
<PageHeader balance={screenSize === ScreenSize.Mobile}> <PageHeader balance={screenSize === ScreenSize.Mobile}>
<Box grow="Yes" gap="300"> <Box grow="Yes" gap="300">
@@ -430,27 +424,6 @@ export function RoomViewHeader() {
</IconButton> </IconButton>
)} )}
</TooltipProvider> </TooltipProvider>
<TooltipProvider
position="Bottom"
offset={4}
tooltip={
<Tooltip>
<Text>My Threads</Text>
</Tooltip>
}
>
{(triggerRef) => (
<IconButton
style={{ position: 'relative' }}
onClick={handleOpenThreadsMenu}
ref={triggerRef}
aria-pressed={!!threadsMenuAnchor}
>
<Icon size="400" src={Icons.Thread} filled={!!threadsMenuAnchor} />
</IconButton>
)}
</TooltipProvider>
<PopOut <PopOut
anchor={pinMenuAnchor} anchor={pinMenuAnchor}
position="Bottom" position="Bottom"
@@ -470,25 +443,6 @@ export function RoomViewHeader() {
</FocusTrap> </FocusTrap>
} }
/> />
<PopOut
anchor={threadsMenuAnchor}
position="Bottom"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
returnFocusOnDeactivate: false,
onDeactivate: () => setThreadsMenuAnchor(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<ThreadsMenu room={room} requestClose={() => setThreadsMenuAnchor(undefined)} />
</FocusTrap>
}
/>
{screenSize === ScreenSize.Desktop && ( {screenSize === ScreenSize.Desktop && (
<TooltipProvider <TooltipProvider
position="Bottom" position="Bottom"
@@ -1,83 +0,0 @@
import { Box, Icon, Icons, Line, Text } from 'folds';
import React, { ReactNode } from 'react';
import classNames from 'classnames';
import { IThreadBundledRelationship, Room } from 'matrix-js-sdk';
import * as css from './styles.css';
import { getMemberDisplayName } from '../../../../utils/room';
import { getMxIdLocalPart } from '../../../../utils/matrix';
import { Time } from '../../../../components/message';
export function ThreadSelectorContainer({ children }: { children: ReactNode }) {
return <Box className={css.ThreadSelectorContainer}>{children}</Box>;
}
type ThreadSelectorProps = {
room: Room;
threadId: string;
threadDetail: IThreadBundledRelationship;
outlined?: boolean;
hour24Clock: boolean;
dateFormatString: string;
onClick?: (threadId: string) => void;
};
export function ThreadSelector({
room,
threadId,
threadDetail,
outlined,
hour24Clock,
dateFormatString,
onClick,
}: ThreadSelectorProps) {
const latestEvent = threadDetail.latest_event;
const latestSenderId = latestEvent.sender;
const latestDisplayName =
getMemberDisplayName(room, latestSenderId) ??
getMxIdLocalPart(latestSenderId) ??
latestSenderId;
const latestEventTs = latestEvent.origin_server_ts;
return (
<Box
as="button"
type="button"
className={classNames(css.ThreadSelector, outlined && css.ThreadSectorOutlined)}
alignItems="Center"
gap="300"
onClick={() => onClick?.(threadId)}
>
<Box className={css.ThreadRepliesCount} shrink="No" alignItems="Center" gap="200">
<Icon size="100" src={Icons.Thread} filled />
<Text size="L400">
{threadDetail.count} {threadDetail.count === 1 ? 'Reply' : 'Replies'}
</Text>
</Box>
{latestSenderId && (
<>
<Line
className={css.ThreadSelectorDivider}
direction="Vertical"
variant="SurfaceVariant"
/>
<Box gap="200" alignItems="Inherit">
<Text size="T200" truncate>
<span>Last reply by </span>
<b>{latestDisplayName}</b>
<span> </span>
<Time
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
ts={latestEventTs}
inheritPriority
/>
</Text>
<Icon size="100" src={Icons.ChevronRight} />
</Box>
</>
)}
</Box>
);
}
@@ -1 +0,0 @@
export * from './ThreadSelector';
@@ -1,37 +0,0 @@
import { style } from '@vanilla-extract/css';
import { color, config, toRem } from 'folds';
import { ContainerColor } from '../../../../styles/ContainerColor.css';
export const ThreadSelectorContainer = style({
marginTop: config.space.S200,
});
export const ThreadSelector = style([
ContainerColor({ variant: 'SurfaceVariant' }),
{
padding: config.space.S200,
borderRadius: config.radii.R400,
cursor: 'pointer',
selectors: {
'&:hover, &:focus-visible': {
backgroundColor: color.SurfaceVariant.ContainerHover,
},
'&:active': {
backgroundColor: color.SurfaceVariant.ContainerActive,
},
},
},
]);
export const ThreadSectorOutlined = style({
borderWidth: config.borderWidth.B300,
});
export const ThreadSelectorDivider = style({
height: toRem(16),
});
export const ThreadRepliesCount = style({
color: color.Primary.Main,
});
@@ -1,79 +0,0 @@
import React from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text, Tooltip, TooltipProvider } from 'folds';
import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
import { Page, PageHeader } from '../../../components/page';
import * as css from './styles.css';
import { useRoom } from '../../../hooks/useRoom';
import { useThreadClose } from '../../../state/hooks/roomToActiveThread';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
type ThreadViewProps = {
threadId: string;
};
export function ThreadView({ threadId }: ThreadViewProps) {
const room = useRoom();
const screenSize = useScreenSizeContext();
const floating = screenSize !== ScreenSize.Desktop;
const closeThread = useThreadClose(room.roomId);
const thread = room.getThread(threadId);
const events = thread?.events ?? [];
return (
<FocusTrap
paused={!floating}
focusTrapOptions={{
initialFocus: false,
clickOutsideDeactivates: true,
onDeactivate: floating ? closeThread : undefined,
}}
>
<Page
className={classNames(css.ThreadView, {
[css.ThreadViewFloating]: floating,
})}
>
<PageHeader>
<Box grow="Yes" alignItems="Center" gap="200">
<Box grow="Yes">
<Text size="H5" truncate>
Thread
</Text>
</Box>
<Box shrink="No" alignItems="Center">
<TooltipProvider
position="Bottom"
align="End"
offset={4}
tooltip={
<Tooltip>
<Text>Close</Text>
</Tooltip>
}
>
{(triggerRef) => (
<IconButton ref={triggerRef} variant="Surface" onClick={closeThread}>
<Icon src={Icons.Cross} />
</IconButton>
)}
</TooltipProvider>
</Box>
</Box>
</PageHeader>
<Box grow="Yes" direction="Column">
<Scroll visibility="Hover" hideTrack>
<div>
{events.map((mEvent) => (
<p style={{ padding: `8px 16px` }} key={mEvent.getId()}>
{mEvent.sender?.name}: {mEvent.getContent().body}
</p>
))}
</div>
</Scroll>
</Box>
</Page>
</FocusTrap>
);
}
@@ -1 +0,0 @@
export * from './ThreadView';
@@ -1,21 +0,0 @@
import { style } from '@vanilla-extract/css';
import { config, toRem } from 'folds';
export const ThreadView = style({
width: toRem(456),
flexShrink: 0,
flexGrow: 0,
});
export const ThreadViewFloating = style({
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
zIndex: 1,
maxWidth: toRem(456),
flexShrink: 1,
width: '100vw',
boxShadow: config.shadow.E400,
});
@@ -1,32 +0,0 @@
import React from 'react';
import { Box, Icon, Icons, toRem, Text, config } from 'folds';
import { ContainerColor } from '../../../styles/ContainerColor.css';
import { BreakWord } from '../../../styles/Text.css';
export function ThreadsError({ error }: { error: Error }) {
return (
<Box
className={ContainerColor({ variant: 'SurfaceVariant' })}
style={{
marginBottom: config.space.S200,
padding: `${config.space.S700} ${config.space.S400} ${toRem(60)}`,
borderRadius: config.radii.R300,
}}
grow="Yes"
direction="Column"
gap="400"
justifyContent="Center"
alignItems="Center"
>
<Icon src={Icons.Warning} size="600" />
<Box style={{ maxWidth: toRem(300) }} direction="Column" gap="200" alignItems="Center">
<Text size="H4" align="Center">
{error.name}
</Text>
<Text className={BreakWord} size="T400" align="Center">
{error.message}
</Text>
</Box>
</Box>
);
}
@@ -1,23 +0,0 @@
import { Box, config, Spinner } from 'folds';
import React from 'react';
import { ContainerColor } from '../../../styles/ContainerColor.css';
export function ThreadsLoading() {
return (
<Box
className={ContainerColor({ variant: 'SurfaceVariant' })}
style={{
marginBottom: config.space.S200,
padding: config.space.S700,
borderRadius: config.radii.R300,
}}
grow="Yes"
direction="Column"
gap="400"
justifyContent="Center"
alignItems="Center"
>
<Spinner variant="Secondary" />
</Box>
);
}
@@ -1,18 +0,0 @@
import { style } from '@vanilla-extract/css';
import { config, toRem } from 'folds';
export const ThreadsMenu = style({
display: 'flex',
maxWidth: toRem(548),
width: '100vw',
maxHeight: '90vh',
});
export const ThreadsMenuHeader = style({
paddingLeft: config.space.S400,
paddingRight: config.space.S200,
});
export const ThreadsMenuContent = style({
paddingLeft: config.space.S200,
});
@@ -1,99 +0,0 @@
/* eslint-disable react/destructuring-assignment */
import React, { forwardRef, useMemo } from 'react';
import { EventTimelineSet, Room } from 'matrix-js-sdk';
import { Box, config, Header, Icon, IconButton, Icons, Menu, Scroll, Text, toRem } from 'folds';
import * as css from './ThreadsMenu.css';
import { ContainerColor } from '../../../styles/ContainerColor.css';
import { useRoomMyThreads } from '../../../hooks/useRoomThreads';
import { AsyncStatus } from '../../../hooks/useAsyncCallback';
import { getLinkedTimelines, getTimelinesEventsCount } from '../utils';
import { ThreadsTimeline } from './ThreadsTimeline';
import { ThreadsLoading } from './ThreadsLoading';
import { ThreadsError } from './ThreadsError';
const getTimelines = (timelineSet: EventTimelineSet) => {
const liveTimeline = timelineSet.getLiveTimeline();
const linkedTimelines = getLinkedTimelines(liveTimeline);
return linkedTimelines;
};
function NoThreads() {
return (
<Box
className={ContainerColor({ variant: 'SurfaceVariant' })}
style={{
marginBottom: config.space.S200,
padding: `${config.space.S700} ${config.space.S400} ${toRem(60)}`,
borderRadius: config.radii.R300,
}}
grow="Yes"
direction="Column"
gap="400"
justifyContent="Center"
alignItems="Center"
>
<Icon src={Icons.Thread} size="600" />
<Box style={{ maxWidth: toRem(300) }} direction="Column" gap="200" alignItems="Center">
<Text size="H4" align="Center">
No Threads Yet
</Text>
<Text size="T400" align="Center">
Threads youre participating in will appear here.
</Text>
</Box>
</Box>
);
}
type ThreadsMenuProps = {
room: Room;
requestClose: () => void;
};
export const ThreadsMenu = forwardRef<HTMLDivElement, ThreadsMenuProps>(
({ room, requestClose }, ref) => {
const threadsState = useRoomMyThreads(room);
const threadsTimelineSet =
threadsState.status === AsyncStatus.Success ? threadsState.data : undefined;
const linkedTimelines = useMemo(() => {
if (!threadsTimelineSet) return undefined;
return getTimelines(threadsTimelineSet);
}, [threadsTimelineSet]);
const hasEvents = linkedTimelines && getTimelinesEventsCount(linkedTimelines) > 0;
return (
<Menu ref={ref} className={css.ThreadsMenu}>
<Box grow="Yes" direction="Column">
<Header className={css.ThreadsMenuHeader} size="500">
<Box grow="Yes">
<Text size="H5">My Threads</Text>
</Box>
<Box shrink="No">
<IconButton size="300" onClick={requestClose} radii="300">
<Icon src={Icons.Cross} size="400" />
</IconButton>
</Box>
</Header>
<Box grow="Yes">
{threadsState.status === AsyncStatus.Success && hasEvents ? (
<ThreadsTimeline timelines={linkedTimelines} requestClose={requestClose} />
) : (
<Scroll size="300" hideTrack visibility="Hover">
<Box className={css.ThreadsMenuContent} direction="Column" gap="100">
{(threadsState.status === AsyncStatus.Loading ||
threadsState.status === AsyncStatus.Idle) && <ThreadsLoading />}
{threadsState.status === AsyncStatus.Success && !hasEvents && <NoThreads />}
{threadsState.status === AsyncStatus.Error && (
<ThreadsError error={threadsState.error} />
)}
</Box>
</Scroll>
)}
</Box>
</Box>
</Menu>
);
}
);
@@ -1,488 +0,0 @@
/* eslint-disable react/destructuring-assignment */
import React, { MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Direction, EventTimeline, MatrixEvent, Room } from 'matrix-js-sdk';
import { Avatar, Box, Chip, config, Icon, Icons, Scroll, Text } from 'folds';
import { Opts as LinkifyOpts } from 'linkifyjs';
import { HTMLReactParserOptions } from 'html-react-parser';
import { useVirtualizer } from '@tanstack/react-virtual';
import { SequenceCard } from '../../../components/sequence-card';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import {
AvatarBase,
ImageContent,
MessageNotDecryptedContent,
MessageUnsupportedContent,
ModernLayout,
MSticker,
RedactedContent,
Reply,
Time,
Username,
UsernameBold,
} from '../../../components/message';
import { UserAvatar } from '../../../components/user-avatar';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import {
getEditedEvent,
getEventThreadDetail,
getMemberAvatarMxc,
getMemberDisplayName,
} from '../../../utils/room';
import { GetContentCallback, MessageEvent } from '../../../../types/matrix/room';
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 { usePowerLevelsContext } from '../../../hooks/usePowerLevels';
import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
import { useTheme } from '../../../hooks/useTheme';
import { PowerIcon } from '../../../components/power';
import colorMXID from '../../../../util/colorMXID';
import { useIsDirectRoom, useRoom } from '../../../hooks/useRoom';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import {
GetMemberPowerTag,
getPowerTagIconSrc,
useAccessiblePowerTagColors,
useGetMemberPowerTag,
} from '../../../hooks/useMemberPowerTag';
import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
import { ThreadSelector, ThreadSelectorContainer } from '../message/thread-selector';
import {
getTimelineAndBaseIndex,
getTimelineEvent,
getTimelineRelativeIndex,
getTimelinesEventsCount,
} from '../utils';
import { ThreadsLoading } from './ThreadsLoading';
import { VirtualTile } from '../../../components/virtualizer';
import { useAlive } from '../../../hooks/useAlive';
import * as css from './ThreadsMenu.css';
type ThreadMessageProps = {
room: Room;
event: MatrixEvent;
renderContent: RenderMatrixEvent<[string, MatrixEvent, string, GetContentCallback]>;
onOpen: (roomId: string, eventId: string) => void;
getMemberPowerTag: GetMemberPowerTag;
accessibleTagColors: Map<string, string>;
legacyUsernameColor: boolean;
hour24Clock: boolean;
dateFormatString: string;
};
function ThreadMessage({
room,
event,
renderContent,
onOpen,
getMemberPowerTag,
accessibleTagColors,
legacyUsernameColor,
hour24Clock,
dateFormatString,
}: ThreadMessageProps) {
const useAuthentication = useMediaAuthentication();
const mx = useMatrixClient();
const handleOpenClick: MouseEventHandler = (evt) => {
evt.stopPropagation();
const evtId = evt.currentTarget.getAttribute('data-event-id');
if (!evtId) return;
onOpen(room.roomId, evtId);
};
const renderOptions = () => (
<Box shrink="No" gap="200" alignItems="Center">
<Chip
data-event-id={event.getId()}
onClick={handleOpenClick}
variant="Secondary"
radii="Pill"
>
<Text size="T200">Open</Text>
</Chip>
</Box>
);
const sender = event.getSender()!;
const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender;
const senderAvatarMxc = getMemberAvatarMxc(room, sender);
const getContent = (() => event.getContent()) as GetContentCallback;
const memberPowerTag = getMemberPowerTag(sender);
const tagColor = memberPowerTag?.color
? accessibleTagColors?.get(memberPowerTag.color)
: undefined;
const tagIconSrc = memberPowerTag?.icon
? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
: undefined;
const usernameColor = legacyUsernameColor ? colorMXID(sender) : tagColor;
const mEventId = event.getId();
if (!mEventId) return null;
return (
<ModernLayout
before={
<AvatarBase>
<Avatar size="300">
<UserAvatar
userId={sender}
src={
senderAvatarMxc
? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ??
undefined
: undefined
}
alt={displayName}
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
/>
</Avatar>
</AvatarBase>
}
>
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
<Box gap="200" alignItems="Baseline">
<Box alignItems="Center" gap="200">
<Username style={{ color: usernameColor }}>
<Text as="span" truncate>
<UsernameBold>{displayName}</UsernameBold>
</Text>
</Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box>
<Time ts={event.getTs()} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
</Box>
{renderOptions()}
</Box>
{event.replyEventId && (
<Reply
room={room}
replyEventId={event.replyEventId}
threadRootId={event.threadRootId}
onClick={handleOpenClick}
getMemberPowerTag={getMemberPowerTag}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor}
/>
)}
{renderContent(event.getType(), false, mEventId, event, displayName, getContent)}
</ModernLayout>
);
}
type ThreadsTimelineProps = {
timelines: EventTimeline[];
requestClose: () => void;
};
export function ThreadsTimeline({ timelines, requestClose }: ThreadsTimelineProps) {
const mx = useMatrixClient();
const { navigateRoom } = useRoomNavigate();
const alive = useAlive();
const scrollRef = useRef<HTMLDivElement>(null);
const room = useRoom();
const powerLevels = usePowerLevelsContext();
const creators = useRoomCreators(room);
const creatorsTag = useRoomCreatorsTag();
const powerLevelTags = usePowerLevelTags(room, powerLevels);
const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
const theme = useTheme();
const accessibleTagColors = useAccessiblePowerTagColors(theme.kind, creatorsTag, powerLevelTags);
const useAuthentication = useMediaAuthentication();
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
const direct = useIsDirectRoom();
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const mentionClickHandler = useMentionClickHandler(room.roomId);
const spoilerClickHandler = useSpoilerClickHandler();
const linkifyOpts = useMemo<LinkifyOpts>(
() => ({
...LINKIFY_OPTS,
render: factoryRenderLinkifyWithMention((href) =>
renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
),
}),
[mx, room, mentionClickHandler]
);
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
() =>
getReactCustomHtmlParser(mx, room.roomId, {
linkifyOpts,
useAuthentication,
handleSpoilerClick: spoilerClickHandler,
handleMentionClick: mentionClickHandler,
}),
[mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication]
);
const renderMatrixEvent = useMatrixEventRenderer<
[string, MatrixEvent, string, GetContentCallback]
>(
{
[MessageEvent.RoomMessage]: (eventId, event, displayName, getContent) => {
if (event.isRedacted()) {
return <RedactedContent reason={event.getUnsigned().redacted_because?.content.reason} />;
}
const threadDetail = getEventThreadDetail(event);
return (
<>
<RenderMessageContent
displayName={displayName}
msgType={event.getContent().msgtype ?? ''}
ts={event.getTs()}
getContent={getContent}
edited={!!event.replacingEvent()}
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
outlineAttachment
/>
{threadDetail && (
<ThreadSelectorContainer>
<ThreadSelector
room={room}
threadId={eventId}
threadDetail={threadDetail}
outlined
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
</ThreadSelectorContainer>
)}
</>
);
},
[MessageEvent.RoomMessageEncrypted]: (eventId, mEvent, displayName) => {
const evtTimeline = room.getTimelineForEvent(eventId);
return (
<EncryptedContent mEvent={mEvent}>
{() => {
if (mEvent.isRedacted()) return <RedactedContent />;
if (mEvent.getType() === MessageEvent.Sticker)
return (
<MSticker
content={mEvent.getContent()}
renderImageContent={(props) => (
<ImageContent
{...props}
autoPlay={mediaAutoLoad}
renderImage={(p) => <Image {...p} loading="lazy" />}
renderViewer={(p) => <ImageViewer {...p} />}
/>
)}
/>
);
if (mEvent.getType() === MessageEvent.RoomMessage) {
const editedEvent =
evtTimeline && getEditedEvent(eventId, mEvent, evtTimeline.getTimelineSet());
const getContent = (() =>
editedEvent?.getContent()['m.new_content'] ??
mEvent.getContent()) as GetContentCallback;
return (
<RenderMessageContent
displayName={displayName}
msgType={mEvent.getContent().msgtype ?? ''}
ts={mEvent.getTs()}
edited={!!editedEvent || !!mEvent.replacingEvent()}
getContent={getContent}
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/>
);
}
if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
return (
<Text>
<MessageNotDecryptedContent />
</Text>
);
return (
<Text>
<MessageUnsupportedContent />
</Text>
);
}}
</EncryptedContent>
);
},
[MessageEvent.Sticker]: (eventId, event, displayName, getContent) => {
if (event.isRedacted()) {
return <RedactedContent reason={event.getUnsigned().redacted_because?.content.reason} />;
}
const threadDetail = getEventThreadDetail(event);
return (
<>
<MSticker
content={getContent()}
renderImageContent={(props) => (
<ImageContent
{...props}
autoPlay={mediaAutoLoad}
renderImage={(p) => <Image {...p} loading="lazy" />}
renderViewer={(p) => <ImageViewer {...p} />}
/>
)}
/>
{threadDetail && (
<ThreadSelectorContainer>
<ThreadSelector
room={room}
threadId={eventId}
threadDetail={threadDetail}
outlined
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
</ThreadSelectorContainer>
)}
</>
);
},
},
undefined,
(eventId, event) => {
if (event.isRedacted()) {
return <RedactedContent reason={event.getUnsigned().redacted_because?.content.reason} />;
}
return (
<Box grow="Yes" direction="Column">
<Text size="T400" priority="300">
<code className={customHtmlCss.Code}>{event.getType()}</code>
{' event'}
</Text>
</Box>
);
}
);
const handleOpen = (roomId: string, eventId: string) => {
navigateRoom(roomId, eventId);
requestClose();
};
const eventsLength = getTimelinesEventsCount(timelines);
const timelineToPaginate = timelines[timelines.length - 1];
const [paginationToken, setPaginationToken] = useState(
timelineToPaginate.getPaginationToken(Direction.Backward)
);
const virtualizer = useVirtualizer({
count: eventsLength,
getScrollElement: () => scrollRef.current,
estimateSize: () => 122,
overscan: 10,
});
const vItems = virtualizer.getVirtualItems();
const paginate = useCallback(async () => {
const moreToLoad = await mx.paginateEventTimeline(timelineToPaginate, {
backwards: true,
limit: 30,
});
if (alive()) {
setPaginationToken(
moreToLoad ? timelineToPaginate.getPaginationToken(Direction.Backward) : null
);
}
}, [mx, alive, timelineToPaginate]);
// auto paginate when scroll reach bottom
useEffect(() => {
const lastVItem = vItems.length > 0 ? vItems[vItems.length - 1] : undefined;
if (paginationToken && lastVItem && lastVItem.index === eventsLength - 1) {
paginate();
}
}, [vItems, paginationToken, eventsLength, paginate]);
return (
<Scroll ref={scrollRef} size="300" hideTrack visibility="Hover">
<Box className={css.ThreadsMenuContent} direction="Column" gap="100">
<div
style={{
position: 'relative',
height: virtualizer.getTotalSize(),
}}
>
{vItems.map((vItem) => {
const reverseTimelineIndex = eventsLength - vItem.index - 1;
const [timeline, baseIndex] = getTimelineAndBaseIndex(timelines, reverseTimelineIndex);
if (!timeline) return null;
const event = getTimelineEvent(
timeline,
getTimelineRelativeIndex(reverseTimelineIndex, baseIndex)
);
if (!event?.getId()) return null;
return (
<VirtualTile
virtualItem={vItem}
style={{ paddingBottom: config.space.S200 }}
ref={virtualizer.measureElement}
key={vItem.index}
>
<SequenceCard
key={event.getId()}
style={{ padding: config.space.S400, borderRadius: config.radii.R300 }}
variant="SurfaceVariant"
direction="Column"
>
<ThreadMessage
room={room}
event={event}
renderContent={renderMatrixEvent}
onOpen={handleOpen}
getMemberPowerTag={getMemberPowerTag}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
</SequenceCard>
</VirtualTile>
);
})}
</div>
{paginationToken && <ThreadsLoading />}
</Box>
</Scroll>
);
}
@@ -1 +0,0 @@
export * from './ThreadsMenu';
-1
View File
@@ -1 +0,0 @@
export * from './timeline';
-66
View File
@@ -1,66 +0,0 @@
import { Room, EventTimeline, Direction, MatrixEvent } from 'matrix-js-sdk';
export const getFirstLinkedTimeline = (
timeline: EventTimeline,
direction: Direction
): EventTimeline => {
const linkedTm = timeline.getNeighbouringTimeline(direction);
if (!linkedTm) return timeline;
return getFirstLinkedTimeline(linkedTm, direction);
};
export const getLinkedTimelines = (timeline: EventTimeline): EventTimeline[] => {
const firstTimeline = getFirstLinkedTimeline(timeline, Direction.Backward);
const timelines: EventTimeline[] = [];
for (
let nextTimeline: EventTimeline | null = firstTimeline;
nextTimeline;
nextTimeline = nextTimeline.getNeighbouringTimeline(Direction.Forward)
) {
timelines.push(nextTimeline);
}
return timelines;
};
export const timelineToEventsCount = (t: EventTimeline) => t.getEvents().length;
export const getTimelinesEventsCount = (timelines: EventTimeline[]): number => {
const timelineEventCountReducer = (count: number, tm: EventTimeline) =>
count + timelineToEventsCount(tm);
return timelines.reduce(timelineEventCountReducer, 0);
};
export const getTimelineAndBaseIndex = (
timelines: EventTimeline[],
index: number
): [EventTimeline | undefined, number] => {
let uptoTimelineLen = 0;
const timeline = timelines.find((t) => {
uptoTimelineLen += t.getEvents().length;
if (index < uptoTimelineLen) return true;
return false;
});
if (!timeline) return [undefined, 0];
return [timeline, uptoTimelineLen - timeline.getEvents().length];
};
export const getTimelineRelativeIndex = (absoluteIndex: number, timelineBaseIndex: number) =>
absoluteIndex - timelineBaseIndex;
export const getTimelineEvent = (timeline: EventTimeline, index: number): MatrixEvent | undefined =>
timeline.getEvents()[index];
export const getEventIdAbsoluteIndex = (
timelines: EventTimeline[],
eventTimeline: EventTimeline,
eventId: string
): number | undefined => {
const timelineIndex = timelines.findIndex((t) => t === eventTimeline);
if (timelineIndex === -1) return undefined;
const eventIndex = eventTimeline.getEvents().findIndex((evt) => evt.getId() === eventId);
if (eventIndex === -1) return undefined;
const baseIndex = timelines
.slice(0, timelineIndex)
.reduce((accValue, timeline) => timeline.getEvents().length + accValue, 0);
return baseIndex + eventIndex;
};
+1 -1
View File
@@ -46,7 +46,7 @@ export function About({ requestClose }: AboutProps) {
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
<Box gap="100" alignItems="End"> <Box gap="100" alignItems="End">
<Text size="H3">Cinny</Text> <Text size="H3">Cinny</Text>
<Text size="T200">v4.10.0</Text> <Text size="T200">v4.10.4</Text>
</Box> </Box>
<Text>Yet another matrix client.</Text> <Text>Yet another matrix client.</Text>
</Box> </Box>
@@ -55,7 +55,7 @@ export function General({ requestClose }: GeneralProps) {
<RoomLocalAddresses permissions={permissions} /> <RoomLocalAddresses permissions={permissions} />
</Box> </Box>
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
<Text size="L400">Advance Options</Text> <Text size="L400">Advanced Options</Text>
<RoomUpgrade permissions={permissions} requestClose={requestClose} /> <RoomUpgrade permissions={permissions} requestClose={requestClose} />
</Box> </Box>
</Box> </Box>
@@ -125,6 +125,13 @@ export const usePermissionGroups = (): PermissionGroup[] => {
const otherSettingsGroup: PermissionGroup = { const otherSettingsGroup: PermissionGroup = {
name: 'Other', name: 'Other',
items: [ items: [
{
location: {
state: true,
key: StateEvent.PoniesRoomEmotes,
},
name: 'Manage Emojis & Stickers',
},
{ {
location: { location: {
state: true, state: true,
+2 -2
View File
@@ -18,7 +18,7 @@ export const useSelectedSpace = (): string | undefined => {
export const useSpaceLobbySelected = (spaceIdOrAlias: string): boolean => { export const useSpaceLobbySelected = (spaceIdOrAlias: string): boolean => {
const match = useMatch({ const match = useMatch({
path: getSpaceLobbyPath(spaceIdOrAlias), path: decodeURIComponent(getSpaceLobbyPath(spaceIdOrAlias)),
caseSensitive: true, caseSensitive: true,
end: false, end: false,
}); });
@@ -28,7 +28,7 @@ export const useSpaceLobbySelected = (spaceIdOrAlias: string): boolean => {
export const useSpaceSearchSelected = (spaceIdOrAlias: string): boolean => { export const useSpaceSearchSelected = (spaceIdOrAlias: string): boolean => {
const match = useMatch({ const match = useMatch({
path: getSpaceSearchPath(spaceIdOrAlias), path: decodeURIComponent(getSpaceSearchPath(spaceIdOrAlias)),
caseSensitive: true, caseSensitive: true,
end: false, end: false,
}); });
+5 -2
View File
@@ -47,7 +47,10 @@ export const useMemberSort = (index: number, memberSort: MemberSortItem[]): Memb
return item; return item;
}; };
export const useMemberPowerSort = (creators: Set<string>): MemberSortFn => { export const useMemberPowerSort = (
creators: Set<string>,
getPowerLevel: (userId: string) => number
): MemberSortFn => {
const sort: MemberSortFn = useCallback( const sort: MemberSortFn = useCallback(
(a, b) => { (a, b) => {
if (creators.has(a.userId) && creators.has(b.userId)) { if (creators.has(a.userId) && creators.has(b.userId)) {
@@ -56,7 +59,7 @@ export const useMemberPowerSort = (creators: Set<string>): MemberSortFn => {
if (creators.has(a.userId)) return -1; if (creators.has(a.userId)) return -1;
if (creators.has(b.userId)) return 1; if (creators.has(b.userId)) return 1;
return b.powerLevel - a.powerLevel; return getPowerLevel(b.userId) - getPowerLevel(a.userId);
}, },
[creators] [creators]
); );
-20
View File
@@ -1,20 +0,0 @@
import { useCallback } from 'react';
import { EventTimelineSet, Room } from 'matrix-js-sdk';
import { AsyncState, useAsyncCallbackValue } from './useAsyncCallback';
export const useRoomMyThreads = (room: Room): AsyncState<EventTimelineSet, Error> => {
const [threadsState] = useAsyncCallbackValue<EventTimelineSet, Error>(
useCallback(async () => {
await room.createThreadsTimelineSets();
await room.fetchRoomThreads();
const timelineSet = room.threadsTimelineSets[0];
if (timelineSet === undefined) {
throw new Error('Failed to fetch My Threads!');
}
return timelineSet;
}, [room])
);
return threadsState;
};
+2 -1
View File
@@ -106,7 +106,8 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
<Route <Route
loader={() => { loader={() => {
if (!getFallbackSession()) { const session = getFallbackSession();
if (!session) {
const afterLoginPath = getAppPathFromHref( const afterLoginPath = getAppPathFromHref(
getOriginBaseUrl(hashRouter), getOriginBaseUrl(hashRouter),
window.location.href window.location.href
+1 -1
View File
@@ -15,7 +15,7 @@ export function AuthFooter() {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
v4.10.0 v4.10.4
</Text> </Text>
<Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer"> <Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
Twitter Twitter
@@ -8,8 +8,6 @@ import { makeNavToActivePathAtom } from '../../state/navToActivePath';
import { NavToActivePathProvider } from '../../state/hooks/navToActivePath'; import { NavToActivePathProvider } from '../../state/hooks/navToActivePath';
import { makeOpenedSidebarFolderAtom } from '../../state/openedSidebarFolder'; import { makeOpenedSidebarFolderAtom } from '../../state/openedSidebarFolder';
import { OpenedSidebarFolderProvider } from '../../state/hooks/openedSidebarFolder'; import { OpenedSidebarFolderProvider } from '../../state/hooks/openedSidebarFolder';
import { makeRoomToActiveThreadAtom } from '../../state/roomToActiveThread';
import { RoomToActiveThreadProvider } from '../../state/hooks/roomToActiveThread';
type ClientInitStorageAtomProps = { type ClientInitStorageAtomProps = {
children: ReactNode; children: ReactNode;
@@ -24,19 +22,15 @@ export function ClientInitStorageAtom({ children }: ClientInitStorageAtomProps)
const navToActivePathAtom = useMemo(() => makeNavToActivePathAtom(userId), [userId]); const navToActivePathAtom = useMemo(() => makeNavToActivePathAtom(userId), [userId]);
const roomToActiveThreadAtom = useMemo(() => makeRoomToActiveThreadAtom(userId), [userId]);
const openedSidebarFolderAtom = useMemo(() => makeOpenedSidebarFolderAtom(userId), [userId]); const openedSidebarFolderAtom = useMemo(() => makeOpenedSidebarFolderAtom(userId), [userId]);
return ( return (
<ClosedNavCategoriesProvider value={closedNavCategoriesAtom}> <ClosedNavCategoriesProvider value={closedNavCategoriesAtom}>
<ClosedLobbyCategoriesProvider value={closedLobbyCategoriesAtom}> <ClosedLobbyCategoriesProvider value={closedLobbyCategoriesAtom}>
<NavToActivePathProvider value={navToActivePathAtom}> <NavToActivePathProvider value={navToActivePathAtom}>
<RoomToActiveThreadProvider value={roomToActiveThreadAtom}>
<OpenedSidebarFolderProvider value={openedSidebarFolderAtom}> <OpenedSidebarFolderProvider value={openedSidebarFolderAtom}>
{children} {children}
</OpenedSidebarFolderProvider> </OpenedSidebarFolderProvider>
</RoomToActiveThreadProvider>
</NavToActivePathProvider> </NavToActivePathProvider>
</ClosedLobbyCategoriesProvider> </ClosedLobbyCategoriesProvider>
</ClosedNavCategoriesProvider> </ClosedNavCategoriesProvider>
+1 -1
View File
@@ -24,7 +24,7 @@ export function WelcomePage() {
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
v4.10.0 v4.10.4
</a> </a>
</span> </span>
} }
-55
View File
@@ -1,55 +0,0 @@
import { createContext, useCallback, useContext } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { RoomToActiveThreadAtom } from '../roomToActiveThread';
const RoomToActiveThreadAtomContext = createContext<RoomToActiveThreadAtom | null>(null);
export const RoomToActiveThreadProvider = RoomToActiveThreadAtomContext.Provider;
export const useRoomToActiveThreadAtom = (): RoomToActiveThreadAtom => {
const anAtom = useContext(RoomToActiveThreadAtomContext);
if (!anAtom) {
throw new Error('RoomToActiveThreadAtom is not provided!');
}
return anAtom;
};
export const useThreadSelector = (roomId: string): ((threadId: string) => void) => {
const roomToActiveThreadAtom = useRoomToActiveThreadAtom();
const setRoomToActiveThread = useSetAtom(roomToActiveThreadAtom);
const onThreadSelect = useCallback(
(threadId: string) => {
setRoomToActiveThread({
type: 'PUT',
roomId,
threadId,
});
},
[roomId, setRoomToActiveThread]
);
return onThreadSelect;
};
export const useActiveThread = (roomId: string): string | undefined => {
const roomToActiveThreadAtom = useRoomToActiveThreadAtom();
const roomToActiveThread = useAtomValue(roomToActiveThreadAtom);
return roomToActiveThread.get(roomId);
};
export const useThreadClose = (roomId: string): (() => void) => {
const roomToActiveThreadAtom = useRoomToActiveThreadAtom();
const setRoomToActiveThread = useSetAtom(roomToActiveThreadAtom);
const closeThread = useCallback(() => {
setRoomToActiveThread({
type: 'DELETE',
roomId,
});
}, [roomId, setRoomToActiveThread]);
return closeThread;
};
-75
View File
@@ -1,75 +0,0 @@
import { atom, WritableAtom } from 'jotai';
import produce from 'immer';
import {
atomWithLocalStorage,
getLocalStorageItem,
setLocalStorageItem,
} from './utils/atomWithLocalStorage';
const ROOM_TO_ACTIVE_THREAD = 'roomToActiveThread';
const getStoreKey = (userId: string): string => `${ROOM_TO_ACTIVE_THREAD}${userId}`;
type RoomToActiveThread = Map<string, string>;
type RoomToActiveThreadAction =
| {
type: 'PUT';
roomId: string;
threadId: string;
}
| {
type: 'DELETE';
roomId: string;
};
export type RoomToActiveThreadAtom = WritableAtom<
RoomToActiveThread,
[RoomToActiveThreadAction],
undefined
>;
export const makeRoomToActiveThreadAtom = (userId: string): RoomToActiveThreadAtom => {
const storeKey = getStoreKey(userId);
const baseRoomToActiveThread = atomWithLocalStorage<RoomToActiveThread>(
storeKey,
(key) => {
const obj: Record<string, string> = getLocalStorageItem(key, {});
return new Map(Object.entries(obj));
},
(key, value) => {
const obj: Record<string, string> = Object.fromEntries(value);
setLocalStorageItem(key, obj);
}
);
const navToActivePathAtom = atom<RoomToActiveThread, [RoomToActiveThreadAction], undefined>(
(get) => get(baseRoomToActiveThread),
(get, set, action) => {
if (action.type === 'DELETE') {
set(
baseRoomToActiveThread,
produce(get(baseRoomToActiveThread), (draft) => {
draft.delete(action.roomId);
})
);
return;
}
if (action.type === 'PUT') {
set(
baseRoomToActiveThread,
produce(get(baseRoomToActiveThread), (draft) => {
draft.set(action.roomId, action.threadId);
})
);
}
}
);
return navToActivePathAtom;
};
export const clearRoomToActiveThreadStore = (userId: string) => {
localStorage.removeItem(getStoreKey(userId));
};
+2 -14
View File
@@ -8,7 +8,6 @@ import {
IPowerLevelsContent, IPowerLevelsContent,
IPushRule, IPushRule,
IPushRules, IPushRules,
IThreadBundledRelationship,
JoinRule, JoinRule,
MatrixClient, MatrixClient,
MatrixEvent, MatrixEvent,
@@ -17,7 +16,6 @@ import {
RelationType, RelationType,
Room, Room,
RoomMember, RoomMember,
THREAD_RELATION_TYPE,
} from 'matrix-js-sdk'; } from 'matrix-js-sdk';
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
import { AccountDataEvent } from '../../types/matrix/accountData'; import { AccountDataEvent } from '../../types/matrix/accountData';
@@ -162,7 +160,8 @@ export const getOrphanParents = (roomToParents: RoomToParents, roomId: string):
}; };
export const isMutedRule = (rule: IPushRule) => export const isMutedRule = (rule: IPushRule) =>
rule.actions[0] === 'dont_notify' && rule.conditions?.[0]?.kind === 'event_match'; // Check for empty actions (new spec) or dont_notify (deprecated)
(rule.actions.length === 0 || rule.actions[0] === 'dont_notify') && rule.conditions?.[0]?.kind === 'event_match';
export const findMutedRule = (overrideRules: IPushRule[], roomId: string) => export const findMutedRule = (overrideRules: IPushRule[], roomId: string) =>
overrideRules.find((rule) => rule.rule_id === roomId && isMutedRule(rule)); overrideRules.find((rule) => rule.rule_id === roomId && isMutedRule(rule));
@@ -203,7 +202,6 @@ export const isNotificationEvent = (mEvent: MatrixEvent) => {
if (mEvent.isRedacted()) return false; if (mEvent.isRedacted()) return false;
if (mEvent.getRelation()?.rel_type === 'm.replace') return false; if (mEvent.getRelation()?.rel_type === 'm.replace') return false;
if (mEvent.getRelation()?.rel_type === 'm.thread') return false;
return true; return true;
}; };
@@ -554,13 +552,3 @@ export const guessPerfectParent = (
return perfectParent; return perfectParent;
}; };
export const getEventThreadDetail = (
mEvent: MatrixEvent
): IThreadBundledRelationship | undefined => {
const details = mEvent.getServerAggregatedRelation<IThreadBundledRelationship>(
THREAD_RELATION_TYPE.name
);
return details;
};
+2 -2
View File
@@ -100,7 +100,7 @@ const transformATag: Transformer = (tagName, attribs) => ({
tagName, tagName,
attribs: { attribs: {
...attribs, ...attribs,
rel: 'noopener', rel: 'noreferrer noopener',
target: '_blank', target: '_blank',
}, },
}); });
@@ -112,7 +112,7 @@ const transformImgTag: Transformer = (tagName, attribs) => {
tagName: 'a', tagName: 'a',
attribs: { attribs: {
href: src, href: src,
rel: 'noopener', rel: 'noreferrer noopener',
target: '_blank', target: '_blank',
}, },
text: attribs.alt || src, text: attribs.alt || src,
+2 -3
View File
@@ -2,7 +2,7 @@ import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from
import { cryptoCallbacks } from './secretStorageKeys'; import { cryptoCallbacks } from './secretStorageKeys';
import { clearNavToActivePathStore } from '../app/state/navToActivePath'; import { clearNavToActivePathStore } from '../app/state/navToActivePath';
import { clearRoomToActiveThreadStore } from '../app/state/roomToActiveThread'; import { pushSessionToSW } from '../sw-session';
type Session = { type Session = {
baseUrl: string; baseUrl: string;
@@ -43,19 +43,18 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
export const startClient = async (mx: MatrixClient) => { export const startClient = async (mx: MatrixClient) => {
await mx.startClient({ await mx.startClient({
lazyLoadMembers: true, lazyLoadMembers: true,
threadSupport: true,
}); });
}; };
export const clearCacheAndReload = async (mx: MatrixClient) => { export const clearCacheAndReload = async (mx: MatrixClient) => {
mx.stopClient(); mx.stopClient();
clearNavToActivePathStore(mx.getSafeUserId()); clearNavToActivePathStore(mx.getSafeUserId());
clearRoomToActiveThreadStore(mx.getSafeUserId());
await mx.store.deleteAllData(); await mx.store.deleteAllData();
window.location.reload(); window.location.reload();
}; };
export const logoutClient = async (mx: MatrixClient) => { export const logoutClient = async (mx: MatrixClient) => {
pushSessionToSW();
mx.stopClient(); mx.stopClient();
try { try {
await mx.logout(); await mx.logout();
+15 -9
View File
@@ -15,6 +15,8 @@ import App from './app/pages/App';
// import i18n (needs to be bundled ;)) // import i18n (needs to be bundled ;))
import './app/i18n'; import './app/i18n';
import { pushSessionToSW } from './sw-session';
import { getFallbackSession } from './app/state/sessions';
document.body.classList.add(configClass, varsClass); document.body.classList.add(configClass, varsClass);
@@ -25,15 +27,19 @@ if ('serviceWorker' in navigator) {
? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js` ? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
: `/dev-sw.js?dev-sw`; : `/dev-sw.js?dev-sw`;
navigator.serviceWorker.register(swUrl); const sendSessionToSW = () => {
navigator.serviceWorker.addEventListener('message', (event) => { const session = getFallbackSession();
if (event.data?.type === 'token' && event.data?.responseKey) { pushSessionToSW(session?.baseUrl, session?.accessToken);
// Get the token for SW. };
const token = localStorage.getItem('cinny_access_token') ?? undefined;
event.source!.postMessage({ navigator.serviceWorker.register(swUrl).then(sendSessionToSW);
responseKey: event.data.responseKey, navigator.serviceWorker.ready.then(sendSessionToSW);
token,
}); navigator.serviceWorker.addEventListener('message', (ev) => {
const { type } = ev.data ?? {};
if (type === 'requestSession') {
sendSessionToSW();
} }
}); });
} }
+10
View File
@@ -0,0 +1,10 @@
export function pushSessionToSW(baseUrl?: string, accessToken?: string) {
if (!('serviceWorker' in navigator)) return;
if (!navigator.serviceWorker.controller) return;
navigator.serviceWorker.controller.postMessage({
type: 'setSession',
accessToken,
baseUrl,
});
}
+134 -28
View File
@@ -3,22 +3,126 @@
export type {}; export type {};
declare const self: ServiceWorkerGlobalScope; declare const self: ServiceWorkerGlobalScope;
async function askForAccessToken(client: Client): Promise<string | undefined> { type SessionInfo = {
return new Promise((resolve) => { accessToken: string;
const responseKey = Math.random().toString(36); baseUrl: string;
const listener = (event: ExtendableMessageEvent) => { };
if (event.data.responseKey !== responseKey) return;
resolve(event.data.token); /**
self.removeEventListener('message', listener); * Store session per client (tab)
}; */
self.addEventListener('message', listener); const sessions = new Map<string, SessionInfo>();
client.postMessage({ responseKey, type: 'token' });
const clientToResolve = new Map<string, (value: SessionInfo | undefined) => void>();
const clientToSessionPromise = new Map<string, Promise<SessionInfo | undefined>>();
async function cleanupDeadClients() {
const activeClients = await self.clients.matchAll();
const activeIds = new Set(activeClients.map((c) => c.id));
Array.from(sessions.keys()).forEach((id) => {
if (!activeIds.has(id)) {
sessions.delete(id);
clientToResolve.delete(id);
clientToSessionPromise.delete(id);
}
}); });
} }
function fetchConfig(token?: string): RequestInit | undefined { function setSession(clientId: string, accessToken: any, baseUrl: any) {
if (!token) return undefined; if (typeof accessToken === 'string' && typeof baseUrl === 'string') {
sessions.set(clientId, { accessToken, baseUrl });
} else {
// Logout or invalid session
sessions.delete(clientId);
}
const resolveSession = clientToResolve.get(clientId);
if (resolveSession) {
resolveSession(sessions.get(clientId));
clientToResolve.delete(clientId);
clientToSessionPromise.delete(clientId);
}
}
function requestSession(client: Client): Promise<SessionInfo | undefined> {
const promise =
clientToSessionPromise.get(client.id) ??
new Promise((resolve) => {
clientToResolve.set(client.id, resolve);
client.postMessage({ type: 'requestSession' });
});
if (!clientToSessionPromise.has(client.id)) {
clientToSessionPromise.set(client.id, promise);
}
return promise;
}
async function requestSessionWithTimeout(
clientId: string,
timeoutMs = 3000
): Promise<SessionInfo | undefined> {
const client = await self.clients.get(clientId);
if (!client) return undefined;
const sessionPromise = requestSession(client);
const timeout = new Promise<undefined>((resolve) => {
setTimeout(() => resolve(undefined), timeoutMs);
});
return Promise.race([sessionPromise, timeout]);
}
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(
(async () => {
await self.clients.claim();
await cleanupDeadClients();
})()
);
});
/**
* Receive session updates from clients
*/
self.addEventListener('message', (event: ExtendableMessageEvent) => {
const client = event.source as Client | null;
if (!client) return;
const { type, accessToken, baseUrl } = event.data || {};
if (type === 'setSession') {
setSession(client.id, accessToken, baseUrl);
cleanupDeadClients();
}
});
const MEDIA_PATHS = ['/_matrix/client/v1/media/download', '/_matrix/client/v1/media/thumbnail'];
function mediaPath(url: string): boolean {
try {
const { pathname } = new URL(url);
return MEDIA_PATHS.some((p) => pathname.startsWith(p));
} catch {
return false;
}
}
function validMediaRequest(url: string, baseUrl: string): boolean {
return MEDIA_PATHS.some((p) => {
const validUrl = new URL(p, baseUrl);
return url.startsWith(validUrl.href);
});
}
function fetchConfig(token: string): RequestInit {
return { return {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
@@ -27,26 +131,28 @@ function fetchConfig(token?: string): RequestInit | undefined {
}; };
} }
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(clients.claim());
});
self.addEventListener('fetch', (event: FetchEvent) => { self.addEventListener('fetch', (event: FetchEvent) => {
const { url, method } = event.request; const { url, method } = event.request;
if (method !== 'GET') return;
if ( if (method !== 'GET' || !mediaPath(url)) return;
!url.includes('/_matrix/client/v1/media/download') &&
!url.includes('/_matrix/client/v1/media/thumbnail') const { clientId } = event;
) { if (!clientId) return;
const session = sessions.get(clientId);
if (session) {
if (validMediaRequest(url, session.baseUrl)) {
event.respondWith(fetch(url, fetchConfig(session.accessToken)));
}
return; return;
} }
event.respondWith(
(async (): Promise<Response> => {
const client = await self.clients.get(event.clientId);
let token: string | undefined;
if (client) token = await askForAccessToken(client);
return fetch(url, fetchConfig(token)); event.respondWith(
})() requestSessionWithTimeout(clientId).then((s) => {
if (s && validMediaRequest(url, s.baseUrl)) {
return fetch(url, fetchConfig(s.accessToken));
}
return fetch(event.request);
})
); );
}); });