Compare commits

..

17 Commits

Author SHA1 Message Date
Ajay Bura 444b2feb9e update folds 2025-11-03 15:30:52 +05:30
Ajay Bura f35aa384e5 thread view - WIP 2025-11-03 15:29:51 +05:30
Ajay Bura b5fd41f862 clear active thread state on logout 2025-11-03 15:28:52 +05:30
Ajay Bura 3d4c91c969 add onClick prop to thread selector 2025-11-03 15:28:42 +05:30
Ajay Bura 38cc6e6f3a add room to active thread atom 2025-11-03 15:27:02 +05:30
Ajay Bura 12bcbc2e78 load threads in My Threads menu 2025-10-22 16:22:31 +05:30
Ajay Bura e44ca92422 remove avatar from threads selector 2025-10-22 16:21:33 +05:30
Ajay Bura 174b315278 move timeline utils functions to new file 2025-10-22 16:21:16 +05:30
Ajay Bura d73428ee3d add option to inherit priority in time component 2025-10-22 16:20:22 +05:30
Ajay Bura f2c5a595b9 Merge branch 'dev' into fix-257 2025-09-27 10:00:30 +05:30
Ajay Bura a6a3ac3b24 redesign thread selector 2025-09-25 12:17:44 +05:30
Ajay Bura 67c6785bf3 inherit font weight for time component 2025-09-25 12:17:14 +05:30
Ajay Bura d36938e1fd fix typo 2025-09-24 16:32:05 +05:30
Ajay Bura 1914606895 threads - WIP 2025-09-24 15:57:15 +05:30
Ajay Bura 19096c3543 Merge branch 'dev' into fix-257 2025-09-21 09:54:55 +05:30
Ajay Bura 737cc09fea Merge branch 'dev' into fix-257 2025-09-15 13:16:59 +05:30
Ajay Bura 154f234d0c thread menu - WIP 2025-09-15 13:16:43 +05:30
176 changed files with 1768 additions and 4866 deletions
+1 -5
View File
@@ -1,10 +1,6 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["config:recommended", ":dependencyDashboardApproval"],
"config:recommended",
":dependencyDashboardApproval",
":semanticCommits"
],
"labels": ["Dependencies"], "labels": ["Dependencies"],
"packageRules": [ "packageRules": [
{ {
+6 -6
View File
@@ -12,12 +12,12 @@ jobs:
PR_NUMBER: ${{github.event.number}} PR_NUMBER: ${{github.event.number}}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4.2.0
- name: Setup node - name: Setup node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".node-version" node-version: 20.12.2
package-manager-cache: false cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Build app - name: Build app
@@ -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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@v4.6.2
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@v4.6.2
with: with:
name: pr name: pr
path: ./pr.txt path: ./pr.txt
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
- name: 'CLA Assistant' - name: 'CLA Assistant'
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release # Beta Release
uses: cla-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 uses: cla-assistant/github-action@v2.6.1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret # the below token should have repo scope and must be manually added by you in the repository's secret
+7 -8
View File
@@ -1,5 +1,4 @@
name: Deploy PR to Netlify name: Deploy PR to Netlify
run-name: "Deploy PR to Netlify (${{ github.event.workflow_run.head_branch }})"
on: on:
workflow_run: workflow_run:
@@ -16,7 +15,7 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }} if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps: steps:
- name: Download pr number - name: Download pr number
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16 uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
with: with:
workflow: ${{ github.event.workflow.id }} workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }} run_id: ${{ github.event.workflow_run.id }}
@@ -25,7 +24,7 @@ jobs:
id: pr id: pr
run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT
- name: Download artifact - name: Download artifact
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16 uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
with: with:
workflow: ${{ github.event.workflow.id }} workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }} run_id: ${{ github.event.workflow_run.id }}
@@ -33,7 +32,7 @@ jobs:
path: dist path: dist
- name: Deploy to Netlify - name: Deploy to Netlify
id: netlify id: netlify
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0 uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
with: with:
publish-dir: dist publish-dir: dist
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}" deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
@@ -46,12 +45,12 @@ jobs:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PR_CINNY }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PR_CINNY }}
timeout-minutes: 1 timeout-minutes: 1
- name: Comment preview on PR - name: Comment preview on PR
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b #v3.0.1 uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6
env: env:
github-token: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
pr-number: ${{ steps.pr.outputs.id }} pr_number: ${{ steps.pr.outputs.id }}
comment-tag: ${{ steps.pr.outputs.id }} comment_tag: ${{ steps.pr.outputs.id }}
message: | message: |
Preview: ${{ steps.netlify.outputs.deploy-url }} Preview: ${{ steps.netlify.outputs.deploy-url }}
⚠️ Exercise caution. Use test accounts. ⚠️ ⚠️ Exercise caution. Use test accounts. ⚠️
+3 -47
View File
@@ -5,59 +5,15 @@ 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
permissions:
contents: read
packages: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4.2.0
- name: Build Docker image
- name: Set up QEMU uses: docker/build-push-action@v6.18.0
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to Docker Hub #Do not update this action from a outside PR
if: github.event.pull_request.head.repo.fork == false
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
continue-on-error: true
- name: Login to the Github Container registry #Do not update this action from a outside PR
if: github.event.pull_request.head.repo.fork == false
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Extract metadata (tags, labels) for Docker, GHCR
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: |
ajbura/cinny
ghcr.io/${{ github.repository }}
- name: Build Docker image (no push)
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with: with:
context: . context: .
platforms: linux/amd64
push: false push: false
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Show Docker images
run: docker images
+2 -2
View File
@@ -14,9 +14,9 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4.2.0
- name: NPM Lockfile Changes - name: NPM Lockfile Changes
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891 # v1.0.0 uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
# Optional inputs, can be deleted safely if you are happy with default values. # Optional inputs, can be deleted safely if you are happy with default values.
+5 -5
View File
@@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4.2.0
- name: Setup node - name: Setup node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".node-version" node-version: 20.12.2
package-manager-cache: false cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Build app - name: Build app
@@ -24,7 +24,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: Deploy to Netlify - name: Deploy to Netlify
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0 uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
with: with:
publish-dir: dist publish-dir: dist
deploy-message: 'Dev deploy ${{ github.sha }}' deploy-message: 'Dev deploy ${{ github.sha }}'
-15
View File
@@ -1,15 +0,0 @@
name: Check PR title
on:
pull_request_target:
types:
- opened
- edited
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+17 -17
View File
@@ -10,12 +10,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4.2.0
- name: Setup node - name: Setup node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".node-version" node-version: 20.12.2
package-manager-cache: false cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Build app - name: Build app
@@ -23,7 +23,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: Deploy to Netlify - name: Deploy to Netlify
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0 uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
with: with:
publish-dir: dist publish-dir: dist
deploy-message: 'Prod deploy ${{ github.ref_name }}' deploy-message: 'Prod deploy ${{ github.ref_name }}'
@@ -52,45 +52,45 @@ jobs:
gpg --export | xxd -p gpg --export | xxd -p
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
- name: Upload tagged release - name: Upload tagged release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
with: with:
files: | files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz cinny-${{ steps.vars.outputs.tag }}.tar.gz
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
publish-image: publish-image:
name: Push Docker image to Docker Hub, GHCR name: Push Docker image to Docker Hub, ghcr
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write packages: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4.2.0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@v3.11.1
- name: Login to Docker Hub #Do not update this action from a outside PR - name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@v3.5.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to the Github Container registry #Do not update this action from a outside PR - name: Login to the Container registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@v3.5.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker, GHCR - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 uses: docker/metadata-action@v5.8.0
with: with:
images: | images: |
${{ 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@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 uses: docker/build-push-action@v6.18.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
-1
View File
@@ -1 +0,0 @@
24.13.1
+2 -2
View File
@@ -1,5 +1,5 @@
## Builder ## Builder
FROM node:24.13.1-alpine AS builder FROM node:20.12.2-alpine3.18 as builder
WORKDIR /src WORKDIR /src
@@ -11,7 +11,7 @@ RUN npm run build
## App ## App
FROM nginx:1.29.5-alpine FROM nginx:1.29.1-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 Krypton LTS (v24.13.1). > 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).
Execute the following commands to start a development server: Execute the following commands to start a development server:
```sh ```sh
+11 -4
View File
@@ -1,6 +1,13 @@
{ {
"defaultHomeserver": 1, "defaultHomeserver": 2,
"homeserverList": ["converser.eu", "matrix.org", "mozilla.org", "unredacted.org", "xmr.se"], "homeserverList": [
"converser.eu",
"envs.net",
"matrix.org",
"monero.social",
"mozilla.org",
"xmr.se"
],
"allowCustomHomeservers": true, "allowCustomHomeservers": true,
"featuredCommunities": { "featuredCommunities": {
@@ -8,7 +15,7 @@
"spaces": [ "spaces": [
"#cinny-space:matrix.org", "#cinny-space:matrix.org",
"#community:matrix.org", "#community:matrix.org",
"#space:unredacted.org", "#space:envs.net",
"#science-space:matrix.org", "#science-space:matrix.org",
"#libregaming-games:tchncs.de", "#libregaming-games:tchncs.de",
"#mathematics-on:matrix.org" "#mathematics-on:matrix.org"
@@ -21,7 +28,7 @@
"#PrivSec.dev:arcticfoxes.net", "#PrivSec.dev:arcticfoxes.net",
"#disroot:aria-net.org" "#disroot:aria-net.org"
], ],
"servers": ["matrix.org", "mozilla.org", "unredacted.org"] "servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
}, },
"hashRouter": { "hashRouter": {
+30 -44
View File
@@ -1,12 +1,12 @@
{ {
"name": "cinny", "name": "cinny",
"version": "4.11.0", "version": "4.10.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cinny", "name": "cinny",
"version": "4.11.0", "version": "4.10.0",
"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.6.2", "folds": "2.5.0",
"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,10 +41,9 @@
"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.3.2", "linkify-react": "4.1.3",
"linkifyjs": "4.3.2", "linkifyjs": "4.1.3",
"matrix-js-sdk": "38.2.0", "matrix-js-sdk": "38.2.0",
"matrix-widget-api": "1.13.0",
"millify": "6.1.0", "millify": "6.1.0",
"pdfjs-dist": "4.2.67", "pdfjs-dist": "4.2.67",
"prismjs": "1.30.0", "prismjs": "1.30.0",
@@ -57,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.30.3", "react-router-dom": "6.20.0",
"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",
@@ -66,7 +65,6 @@
"ua-parser-js": "1.0.35" "ua-parser-js": "1.0.35"
}, },
"devDependencies": { "devDependencies": {
"@element-hq/element-call-embedded": "0.16.3",
"@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@rollup/plugin-inject": "5.0.3", "@rollup/plugin-inject": "5.0.3",
"@rollup/plugin-wasm": "6.1.1", "@rollup/plugin-wasm": "6.1.1",
@@ -1651,12 +1649,6 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@element-hq/element-call-embedded": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@element-hq/element-call-embedded/-/element-call-embedded-0.16.3.tgz",
"integrity": "sha512-OViKJonDaDNVBUW9WdV9mk78/Ruh34C7XsEgt3O8D9z+64C39elbIgllHSoH5S12IRlv9RYrrV37FZLo6QWsDQ==",
"dev": true
},
"node_modules/@emotion/hash": { "node_modules/@emotion/hash": {
"version": "0.9.2", "version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
@@ -3707,10 +3699,9 @@
} }
}, },
"node_modules/@remix-run/router": { "node_modules/@remix-run/router": {
"version": "1.23.2", "version": "1.13.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.0.tgz",
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "integrity": "sha512-5dMOnVnefRsl4uRnAdoWjtVTdh8e6aZqgM4puy9nmEADH72ck+uXwzpJLEKE9Q6F8ZljNewLgmTfkxUrBdv4WA==",
"license": "MIT",
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
@@ -7166,9 +7157,9 @@
} }
}, },
"node_modules/folds": { "node_modules/folds": {
"version": "2.6.2", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/folds/-/folds-2.6.2.tgz", "resolved": "https://registry.npmjs.org/folds/-/folds-2.5.0.tgz",
"integrity": "sha512-1HemxxSnBm8/U5kq1pDQrFkpltWgQN90DmWCZWkZb7D2pe8BhOJSwIRLjk9WxHcw6nn69oz2XNYIXtSw0LvX1w==", "integrity": "sha512-UJhvXAQ1XnZ9w10KJwSW+frvzzWE/zcF0dH3fDVCD70RFHAxwEi0UkkVS8CaZGxZF2Wvt3qTJyTS5LW3LwwUAw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peerDependencies": { "peerDependencies": {
"@vanilla-extract/css": "1.9.2", "@vanilla-extract/css": "1.9.2",
@@ -8500,20 +8491,18 @@
} }
}, },
"node_modules/linkify-react": { "node_modules/linkify-react": {
"version": "4.3.2", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.3.2.tgz", "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz",
"integrity": "sha512-mi744h1hf+WDsr+paJgSBBgYNLMWNSHyM9V9LVUo03RidNGdw1VpI7Twnt+K3pEh3nIzB4xiiAgZxpd61ItKpQ==", "integrity": "sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA==",
"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.3.2", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz",
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg=="
"license": "MIT"
}, },
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
@@ -8674,9 +8663,9 @@
} }
}, },
"node_modules/matrix-widget-api": { "node_modules/matrix-widget-api": {
"version": "1.13.0", "version": "1.13.1",
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.13.0.tgz", "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.13.1.tgz",
"integrity": "sha512-+LrvwkR1izL4h2euX8PDrvG/3PZZDEd6As+lmnR3jAVwbFJtU5iTnwmZGnCca9ddngCvXvAHkcpJBEPyPTZneQ==", "integrity": "sha512-mkOHUVzaN018TCbObfGOSaMW2GoUxOfcxNNlTVx5/HeMk3OSQPQM0C9oEME5Liiv/dBUoSrEB64V8wF7e/gb1w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@types/events": "^3.0.0", "@types/events": "^3.0.0",
@@ -9616,12 +9605,11 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "6.30.3", "version": "6.20.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.20.0.tgz",
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "integrity": "sha512-pVvzsSsgUxxtuNfTHC4IxjATs10UaAtvLGVSA1tbUE4GDaOSU1Esu2xF5nWLz7KPiMuW8BJWuPFdlGYJ7/rW0w==",
"license": "MIT",
"dependencies": { "dependencies": {
"@remix-run/router": "1.23.2" "@remix-run/router": "1.13.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -9631,13 +9619,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "6.30.3", "version": "6.20.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.20.0.tgz",
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", "integrity": "sha512-CbcKjEyiSVpA6UtCHOIYLUYn/UJfwzp55va4yEfpk7JBN3GPqWfHrdLkAvNCcpXr8QoihcDMuk0dzWZxtlB/mQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@remix-run/router": "1.23.2", "@remix-run/router": "1.13.0",
"react-router": "6.30.3" "react-router": "6.20.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -10917,7 +10904,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
+5 -8
View File
@@ -1,6 +1,6 @@
{ {
"name": "cinny", "name": "cinny",
"version": "4.11.0", "version": "4.10.0",
"description": "Yet another matrix client", "description": "Yet another matrix client",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@@ -10,7 +10,6 @@
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview",
"lint": "yarn check:eslint && yarn check:prettier", "lint": "yarn check:eslint && yarn check:prettier",
"check:eslint": "eslint src/*", "check:eslint": "eslint src/*",
"check:prettier": "prettier --check .", "check:prettier": "prettier --check .",
@@ -44,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.6.2", "folds": "2.5.0",
"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",
@@ -53,10 +52,9 @@
"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.3.2", "linkify-react": "4.1.3",
"linkifyjs": "4.3.2", "linkifyjs": "4.1.3",
"matrix-js-sdk": "38.2.0", "matrix-js-sdk": "38.2.0",
"matrix-widget-api": "1.13.0",
"millify": "6.1.0", "millify": "6.1.0",
"pdfjs-dist": "4.2.67", "pdfjs-dist": "4.2.67",
"prismjs": "1.30.0", "prismjs": "1.30.0",
@@ -69,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.30.3", "react-router-dom": "6.20.0",
"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",
@@ -78,7 +76,6 @@
"ua-parser-js": "1.0.35" "ua-parser-js": "1.0.35"
}, },
"devDependencies": { "devDependencies": {
"@element-hq/element-call-embedded": "0.16.3",
"@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@rollup/plugin-inject": "5.0.3", "@rollup/plugin-inject": "5.0.3",
"@rollup/plugin-wasm": "6.1.1", "@rollup/plugin-wasm": "6.1.1",
+2 -6
View File
@@ -51,12 +51,8 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
}, },
location.pathname location.pathname
); );
const encodedSpaceIdOrAlias = spaceMatch?.params.spaceIdOrAlias; if (spaceMatch?.params.spaceIdOrAlias) {
const decodedSpaceIdOrAlias = navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias));
encodedSpaceIdOrAlias && decodeURIComponent(encodedSpaceIdOrAlias);
if (decodedSpaceIdOrAlias) {
navigate(getSpacePath(decodedSpaceIdOrAlias));
return; return;
} }
if ( if (
-66
View File
@@ -1,66 +0,0 @@
import React, { ReactNode, useCallback, useRef } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { config } from 'folds';
import {
CallEmbedContextProvider,
CallEmbedRefContextProvider,
useCallHangupEvent,
useCallJoined,
useCallThemeSync,
useCallMemberSoundSync,
} from '../hooks/useCallEmbed';
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
import { CallEmbed } from '../plugins/call';
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
function CallUtils({ embed }: { embed: CallEmbed }) {
const setCallEmbed = useSetAtom(callEmbedAtom);
useCallMemberSoundSync(embed);
useCallThemeSync(embed);
useCallHangupEvent(
embed,
useCallback(() => {
setCallEmbed(undefined);
}, [setCallEmbed])
);
return null;
}
type CallEmbedProviderProps = {
children?: ReactNode;
};
export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
const callEmbed = useAtomValue(callEmbedAtom);
const callEmbedRef = useRef<HTMLDivElement>(null);
const joined = useCallJoined(callEmbed);
const selectedRoom = useSelectedRoom();
const chat = useAtomValue(callChatAtom);
const screenSize = useScreenSizeContext();
const chatOnlyView = chat && screenSize !== ScreenSize.Desktop;
const callVisible = callEmbed && selectedRoom === callEmbed.roomId && joined && !chatOnlyView;
return (
<CallEmbedContextProvider value={callEmbed}>
{callEmbed && <CallUtils embed={callEmbed} />}
<CallEmbedRefContextProvider value={callEmbedRef}>{children}</CallEmbedRefContextProvider>
<div
data-call-embed-container
style={{
visibility: callVisible ? undefined : 'hidden',
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '50%',
}}
ref={callEmbedRef}
/>
</CallEmbedContextProvider>
);
}
+20 -10
View File
@@ -16,24 +16,34 @@ import {
import { JoinRule } from 'matrix-js-sdk'; import { JoinRule } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { stopPropagation } from '../utils/keyboard'; import { stopPropagation } from '../utils/keyboard';
import { getRoomIconSrc } from '../utils/room';
export type ExtraJoinRules = 'knock_restricted'; export type ExtraJoinRules = 'knock_restricted';
export type ExtendedJoinRules = JoinRule | ExtraJoinRules; export type ExtendedJoinRules = JoinRule | ExtraJoinRules;
type JoinRuleIcons = Record<ExtendedJoinRules, IconSrc>; type JoinRuleIcons = Record<ExtendedJoinRules, IconSrc>;
export const useRoomJoinRuleIcon = (): JoinRuleIcons =>
export const useJoinRuleIcons = (roomType?: string): JoinRuleIcons =>
useMemo( useMemo(
() => ({ () => ({
[JoinRule.Invite]: getRoomIconSrc(Icons, roomType, JoinRule.Invite), [JoinRule.Invite]: Icons.HashLock,
[JoinRule.Knock]: getRoomIconSrc(Icons, roomType, JoinRule.Knock), [JoinRule.Knock]: Icons.HashLock,
knock_restricted: getRoomIconSrc(Icons, roomType, JoinRule.Restricted), knock_restricted: Icons.Hash,
[JoinRule.Restricted]: getRoomIconSrc(Icons, roomType, JoinRule.Restricted), [JoinRule.Restricted]: Icons.Hash,
[JoinRule.Public]: getRoomIconSrc(Icons, roomType, JoinRule.Public), [JoinRule.Public]: Icons.HashGlobe,
[JoinRule.Private]: getRoomIconSrc(Icons, roomType, JoinRule.Private), [JoinRule.Private]: Icons.HashLock,
}), }),
[roomType] []
);
export const useSpaceJoinRuleIcon = (): JoinRuleIcons =>
useMemo(
() => ({
[JoinRule.Invite]: Icons.SpaceLock,
[JoinRule.Knock]: Icons.SpaceLock,
knock_restricted: Icons.Space,
[JoinRule.Restricted]: Icons.Space,
[JoinRule.Public]: Icons.SpaceGlobe,
[JoinRule.Private]: Icons.SpaceLock,
}),
[]
); );
type JoinRuleLabels = Record<ExtendedJoinRules, string>; type JoinRuleLabels = Record<ExtendedJoinRules, string>;
@@ -2,39 +2,43 @@ import React from 'react';
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds'; import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
import { SequenceCard } from '../sequence-card'; import { SequenceCard } from '../sequence-card';
import { SettingTile } from '../setting-tile'; import { SettingTile } from '../setting-tile';
import { CreateRoomAccess } from './types';
type CreateRoomAccessSelectorProps = { export enum CreateRoomKind {
value?: CreateRoomAccess; Private = 'private',
onSelect: (value: CreateRoomAccess) => void; Restricted = 'restricted',
Public = 'public',
}
type CreateRoomKindSelectorProps = {
value?: CreateRoomKind;
onSelect: (value: CreateRoomKind) => void;
canRestrict?: boolean; canRestrict?: boolean;
disabled?: boolean; disabled?: boolean;
getIcon: (access: CreateRoomAccess) => IconSrc; getIcon: (kind: CreateRoomKind) => IconSrc;
}; };
export function CreateRoomAccessSelector({ export function CreateRoomKindSelector({
value, value,
onSelect, onSelect,
canRestrict, canRestrict,
disabled, disabled,
getIcon, getIcon,
}: CreateRoomAccessSelectorProps) { }: CreateRoomKindSelectorProps) {
return ( return (
<Box shrink="No" direction="Column" gap="100"> <Box shrink="No" direction="Column" gap="100">
{canRestrict && ( {canRestrict && (
<SequenceCard <SequenceCard
style={{ padding: config.space.S300 }} style={{ padding: config.space.S300 }}
variant={value === CreateRoomAccess.Restricted ? 'Primary' : 'SurfaceVariant'} variant={value === CreateRoomKind.Restricted ? 'Primary' : 'SurfaceVariant'}
direction="Column" direction="Column"
gap="100" gap="100"
as="button" as="button"
type="button" type="button"
aria-pressed={value === CreateRoomAccess.Restricted} aria-pressed={value === CreateRoomKind.Restricted}
onClick={() => onSelect(CreateRoomAccess.Restricted)} onClick={() => onSelect(CreateRoomKind.Restricted)}
disabled={disabled} disabled={disabled}
> >
<SettingTile <SettingTile
before={<Icon size="400" src={getIcon(CreateRoomAccess.Restricted)} />} before={<Icon size="400" src={getIcon(CreateRoomKind.Restricted)} />}
after={value === CreateRoomAccess.Restricted && <Icon src={Icons.Check} />} after={value === CreateRoomKind.Restricted && <Icon src={Icons.Check} />}
> >
<Text size="H6">Restricted</Text> <Text size="H6">Restricted</Text>
<Text size="T300" priority="300"> <Text size="T300" priority="300">
@@ -45,18 +49,18 @@ export function CreateRoomAccessSelector({
)} )}
<SequenceCard <SequenceCard
style={{ padding: config.space.S300 }} style={{ padding: config.space.S300 }}
variant={value === CreateRoomAccess.Private ? 'Primary' : 'SurfaceVariant'} variant={value === CreateRoomKind.Private ? 'Primary' : 'SurfaceVariant'}
direction="Column" direction="Column"
gap="100" gap="100"
as="button" as="button"
type="button" type="button"
aria-pressed={value === CreateRoomAccess.Private} aria-pressed={value === CreateRoomKind.Private}
onClick={() => onSelect(CreateRoomAccess.Private)} onClick={() => onSelect(CreateRoomKind.Private)}
disabled={disabled} disabled={disabled}
> >
<SettingTile <SettingTile
before={<Icon size="400" src={getIcon(CreateRoomAccess.Private)} />} before={<Icon size="400" src={getIcon(CreateRoomKind.Private)} />}
after={value === CreateRoomAccess.Private && <Icon src={Icons.Check} />} after={value === CreateRoomKind.Private && <Icon src={Icons.Check} />}
> >
<Text size="H6">Private</Text> <Text size="H6">Private</Text>
<Text size="T300" priority="300"> <Text size="T300" priority="300">
@@ -66,18 +70,18 @@ export function CreateRoomAccessSelector({
</SequenceCard> </SequenceCard>
<SequenceCard <SequenceCard
style={{ padding: config.space.S300 }} style={{ padding: config.space.S300 }}
variant={value === CreateRoomAccess.Public ? 'Primary' : 'SurfaceVariant'} variant={value === CreateRoomKind.Public ? 'Primary' : 'SurfaceVariant'}
direction="Column" direction="Column"
gap="100" gap="100"
as="button" as="button"
type="button" type="button"
aria-pressed={value === CreateRoomAccess.Public} aria-pressed={value === CreateRoomKind.Public}
onClick={() => onSelect(CreateRoomAccess.Public)} onClick={() => onSelect(CreateRoomKind.Public)}
disabled={disabled} disabled={disabled}
> >
<SettingTile <SettingTile
before={<Icon size="400" src={getIcon(CreateRoomAccess.Public)} />} before={<Icon size="400" src={getIcon(CreateRoomKind.Public)} />}
after={value === CreateRoomAccess.Public && <Icon src={Icons.Check} />} after={value === CreateRoomKind.Public && <Icon src={Icons.Check} />}
> >
<Text size="H6">Public</Text> <Text size="H6">Public</Text>
<Text size="T300" priority="300"> <Text size="T300" priority="300">
@@ -1,75 +0,0 @@
import React from 'react';
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
import { SequenceCard } from '../sequence-card';
import { SettingTile } from '../setting-tile';
import { CreateRoomType } from './types';
import { BetaNoticeBadge } from '../BetaNoticeBadge';
type CreateRoomTypeSelectorProps = {
value?: CreateRoomType;
onSelect: (value: CreateRoomType) => void;
disabled?: boolean;
getIcon: (type: CreateRoomType) => IconSrc;
};
export function CreateRoomTypeSelector({
value,
onSelect,
disabled,
getIcon,
}: CreateRoomTypeSelectorProps) {
return (
<Box shrink="No" direction="Column" gap="100">
<SequenceCard
style={{ padding: config.space.S300 }}
variant={value === CreateRoomType.TextRoom ? 'Primary' : 'SurfaceVariant'}
direction="Column"
gap="100"
as="button"
type="button"
aria-pressed={value === CreateRoomType.TextRoom}
onClick={() => onSelect(CreateRoomType.TextRoom)}
disabled={disabled}
>
<SettingTile
before={<Icon size="400" src={getIcon(CreateRoomType.TextRoom)} />}
after={value === CreateRoomType.TextRoom && <Icon src={Icons.Check} />}
>
<Box gap="200" alignItems="Baseline">
<Text size="H6" style={{ flexShrink: 0 }}>
Chat Room
</Text>
<Text size="T300" priority="300" truncate>
- Messages, photos, and videos.
</Text>
</Box>
</SettingTile>
</SequenceCard>
<SequenceCard
style={{ padding: config.space.S300 }}
variant={value === CreateRoomType.VoiceRoom ? 'Primary' : 'SurfaceVariant'}
direction="Column"
gap="100"
as="button"
type="button"
aria-pressed={value === CreateRoomType.VoiceRoom}
onClick={() => onSelect(CreateRoomType.VoiceRoom)}
disabled={disabled}
>
<SettingTile
before={<Icon size="400" src={getIcon(CreateRoomType.VoiceRoom)} />}
after={value === CreateRoomType.VoiceRoom && <Icon src={Icons.Check} />}
>
<Box gap="200" alignItems="Baseline">
<Text size="H6" style={{ flexShrink: 0 }}>
Voice Room
</Text>
<Text size="T300" priority="300" truncate>
- Live audio and video conversations.
</Text>
<BetaNoticeBadge />
</Box>
</SettingTile>
</SequenceCard>
</Box>
);
}
+1 -2
View File
@@ -1,6 +1,5 @@
export * from './CreateRoomAccessSelector'; export * from './CreateRoomKindSelector';
export * from './CreateRoomAliasInput'; export * from './CreateRoomAliasInput';
export * from './RoomVersionSelector'; export * from './RoomVersionSelector';
export * from './utils'; export * from './utils';
export * from './AdditionalCreatorInput'; export * from './AdditionalCreatorInput';
export * from './types';
-10
View File
@@ -1,10 +0,0 @@
export enum CreateRoomType {
TextRoom = 'text',
VoiceRoom = 'voice',
}
export enum CreateRoomAccess {
Private = 'private',
Restricted = 'restricted',
Public = 'public',
}
+6 -32
View File
@@ -7,10 +7,10 @@ import {
Room, Room,
} from 'matrix-js-sdk'; } from 'matrix-js-sdk';
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types'; import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
import { CreateRoomKind } from './CreateRoomKindSelector';
import { RoomType, StateEvent } from '../../../types/matrix/room'; import { RoomType, StateEvent } from '../../../types/matrix/room';
import { getViaServers } from '../../plugins/via-servers'; import { getViaServers } from '../../plugins/via-servers';
import { getMxIdServer } from '../../utils/matrix'; import { getMxIdServer } from '../../utils/matrix';
import { CreateRoomAccess } from './types';
export const createRoomCreationContent = ( export const createRoomCreationContent = (
type: RoomType | undefined, type: RoomType | undefined,
@@ -32,7 +32,7 @@ export const createRoomCreationContent = (
}; };
export const createRoomJoinRulesState = ( export const createRoomJoinRulesState = (
access: CreateRoomAccess, kind: CreateRoomKind,
parent: Room | undefined, parent: Room | undefined,
knock: boolean knock: boolean
) => { ) => {
@@ -40,13 +40,13 @@ export const createRoomJoinRulesState = (
join_rule: knock ? JoinRule.Knock : JoinRule.Invite, join_rule: knock ? JoinRule.Knock : JoinRule.Invite,
}; };
if (access === CreateRoomAccess.Public) { if (kind === CreateRoomKind.Public) {
content = { content = {
join_rule: JoinRule.Public, join_rule: JoinRule.Public,
}; };
} }
if (access === CreateRoomAccess.Restricted && parent) { if (kind === CreateRoomKind.Restricted && parent) {
content = { content = {
join_rule: knock ? ('knock_restricted' as JoinRule) : JoinRule.Restricted, join_rule: knock ? ('knock_restricted' as JoinRule) : JoinRule.Restricted,
allow: [ allow: [
@@ -74,10 +74,6 @@ 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: '',
@@ -86,23 +82,11 @@ export const createRoomEncryptionState = () => ({
}, },
}); });
export const createRoomCallState = () => ({
type: 'org.matrix.msc3401.call',
state_key: '',
content: {},
});
export const createVoiceRoomPowerLevelsOverride = () => ({
events: {
[StateEvent.GroupCallMemberPrefix]: 0,
},
});
export type CreateRoomData = { export type CreateRoomData = {
version: string; version: string;
type?: RoomType; type?: RoomType;
parent?: Room; parent?: Room;
access: CreateRoomAccess; kind: CreateRoomKind;
name: string; name: string;
topic?: string; topic?: string;
aliasLocalPart?: string; aliasLocalPart?: string;
@@ -122,11 +106,7 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
initialState.push(createRoomParentState(data.parent)); initialState.push(createRoomParentState(data.parent));
} }
if (data.type === RoomType.Call) { initialState.push(createRoomJoinRulesState(data.kind, data.parent, data.knock));
initialState.push(createRoomCallState());
}
initialState.push(createRoomJoinRulesState(data.access, data.parent, data.knock));
const options: ICreateRoomOpts = { const options: ICreateRoomOpts = {
room_version: data.version, room_version: data.version,
@@ -138,15 +118,9 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
data.allowFederation, data.allowFederation,
data.additionalCreators data.additionalCreators
), ),
power_level_content_override:
data.type === RoomType.Call ? createVoiceRoomPowerLevelsOverride() : undefined,
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,8 +88,6 @@ 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}
@@ -100,11 +98,11 @@ export function EmoticonAutocomplete({
} }
onClick={() => handleAutocomplete(key, emoticon.shortcode)} onClick={() => handleAutocomplete(key, emoticon.shortcode)}
before={ before={
isCustomEmoji && customEmojiUrl ? ( isCustomEmoji ? (
<Box <Box
shrink="No" shrink="No"
as="img" as="img"
src={customEmojiUrl} src={mxcUrlToHttp(mx, key, useAuthentication) || key}
alt={emoticon.shortcode} alt={emoticon.shortcode}
style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }} style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }}
/> />
@@ -169,13 +169,12 @@ export function RoomMentionAutocomplete({
<RoomIcon <RoomIcon
size="50" size="50"
joinRule={room.getJoinRule() ?? JoinRule.Restricted} joinRule={room.getJoinRule() ?? JoinRule.Restricted}
roomType={room.getType()}
filled filled
/> />
)} )}
/> />
) : ( ) : (
<RoomIcon size="100" joinRule={room.getJoinRule()} roomType={room.getType()} /> <RoomIcon size="100" joinRule={room.getJoinRule()} space={room.isSpaceRoom()} />
)} )}
</Avatar> </Avatar>
} }
+1 -2
View File
@@ -212,10 +212,9 @@ 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.name === '@room') { if (node.id === getCanonicalAliasOrRoomId(mx, roomId)) {
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,7 +202,8 @@ 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) ?? undefined; mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ||
pack.meta.avatar;
return ( return (
<ImageGroupIcon <ImageGroupIcon
@@ -265,7 +266,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) ?? undefined; mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || pack.meta.avatar;
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) ?? ''} src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
/> />
</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) ?? ''} src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
/> />
</Box> </Box>
); );
+1 -2
View File
@@ -27,8 +27,7 @@ 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); const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
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);
@@ -389,8 +389,6 @@ export function MLocation({ content }: MLocationProps) {
const geoUri = content.geo_uri; const geoUri = content.geo_uri;
if (typeof geoUri !== 'string') return <BrokenContent />; if (typeof geoUri !== 'string') return <BrokenContent />;
const location = parseGeoUri(geoUri); const location = parseGeoUri(geoUri);
if (!location) return <BrokenContent />;
return ( return (
<Box direction="Column" alignItems="Start" gap="100"> <Box direction="Column" alignItems="Start" gap="100">
<Text size="T400">{geoUri}</Text> <Text size="T400">{geoUri}</Text>
+6
View File
@@ -0,0 +1,6 @@
import { style } from '@vanilla-extract/css';
export const Time = style({
fontWeight: 'inherit',
flexShrink: 0,
});
+13 -3
View File
@@ -1,12 +1,15 @@
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;
}; };
/** /**
@@ -22,7 +25,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, ...props }, ref) => { ({ compact, hour24Clock, dateFormatString, ts, inheritPriority, className, ...props }, ref) => {
const formattedTime = timeHourMinute(ts, hour24Clock); const formattedTime = timeHourMinute(ts, hour24Clock);
let time = ''; let time = '';
@@ -33,11 +36,18 @@ 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 as="time" style={{ flexShrink: 0 }} size="T200" priority="300" {...props} ref={ref}> <Text
as="time"
className={classNames(css.Time, className)}
size="T200"
priority={inheritPriority ? undefined : '300'}
{...props}
ref={ref}
>
{time} {time}
</Text> </Text>
); );
@@ -54,8 +54,7 @@ export function AudioContent({
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication); const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
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,8 +86,7 @@ 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); const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
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);
@@ -177,8 +176,7 @@ 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); const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
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);
@@ -255,8 +253,7 @@ 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); const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
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,8 +87,7 @@ export const ImageContent = as<'div', ImageContentProps>(
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication); const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
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,8 +23,7 @@ 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); const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? thumbMxcUrl;
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,8 +81,7 @@ export const VideoContent = as<'div', VideoContentProps>(
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication); const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
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)
+1
View File
@@ -49,6 +49,7 @@ const NavItemBase = style({
display: 'flex', display: 'flex',
justifyContent: 'start', justifyContent: 'start',
cursor: 'pointer', cursor: 'pointer',
backgroundColor: Container,
color: OnContainer, color: OnContainer,
outline: 'none', outline: 'none',
minHeight: toRem(36), minHeight: toRem(36),
+3 -3
View File
@@ -14,7 +14,7 @@ export function PageRoot({ nav, children }: PageRootProps) {
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
return ( return (
<Box grow="Yes"> <Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
{nav} {nav}
{screenSize !== ScreenSize.Mobile && ( {screenSize !== ScreenSize.Mobile && (
<Line variant="Background" size="300" direction="Vertical" /> <Line variant="Background" size="300" direction="Vertical" />
@@ -79,11 +79,11 @@ export function PageNavContent({
); );
} }
export const Page = as<'div', css.PageVariants>(({ className, transparent, ...props }, ref) => ( export const Page = as<'div'>(({ className, ...props }, ref) => (
<Box <Box
grow="Yes" grow="Yes"
direction="Column" direction="Column"
className={classNames(css.Page({ transparent }), className)} className={classNames(ContainerColor({ variant: 'Surface' }), className)}
{...props} {...props}
ref={ref} ref={ref}
/> />
-14
View File
@@ -1,7 +1,6 @@
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
import { recipe, RecipeVariants } from '@vanilla-extract/recipes'; import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
import { DefaultReset, color, config, toRem } from 'folds'; import { DefaultReset, color, config, toRem } from 'folds';
import { ContainerColor } from '../../styles/ContainerColor.css';
export const PageNav = recipe({ export const PageNav = recipe({
variants: { variants: {
@@ -60,19 +59,6 @@ export const PageNavContent = style({
paddingBottom: config.space.S700, paddingBottom: config.space.S700,
}); });
export const Page = recipe({
base: [ContainerColor({ variant: 'Surface' })],
variants: {
transparent: {
true: {
background: 'transparent',
},
},
},
});
export type PageVariants = RecipeVariants<typeof Page>;
export const PageHeader = recipe({ export const PageHeader = recipe({
base: { base: {
paddingLeft: config.space.S400, paddingLeft: config.space.S400,
@@ -2,7 +2,7 @@ import { JoinRule } from 'matrix-js-sdk';
import { AvatarFallback, AvatarImage, Icon, Icons, color } from 'folds'; import { AvatarFallback, AvatarImage, Icon, Icons, color } from 'folds';
import React, { ComponentProps, ReactEventHandler, ReactNode, forwardRef, useState } from 'react'; import React, { ComponentProps, ReactEventHandler, ReactNode, forwardRef, useState } from 'react';
import * as css from './RoomAvatar.css'; import * as css from './RoomAvatar.css';
import { getRoomIconSrc } from '../../utils/room'; import { joinRuleToIconSrc } from '../../utils/room';
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
type RoomAvatarProps = { type RoomAvatarProps = {
@@ -44,9 +44,13 @@ export function RoomAvatar({ roomId, src, alt, renderFallback }: RoomAvatarProps
export const RoomIcon = forwardRef< export const RoomIcon = forwardRef<
SVGSVGElement, SVGSVGElement,
Omit<ComponentProps<typeof Icon>, 'src'> & { Omit<ComponentProps<typeof Icon>, 'src'> & {
joinRule?: JoinRule; joinRule: JoinRule;
roomType?: string; space?: boolean;
} }
>(({ joinRule, roomType, ...props }, ref) => ( >(({ joinRule, space, ...props }, ref) => (
<Icon src={getRoomIconSrc(Icons, roomType, joinRule)} {...props} ref={ref} /> <Icon
src={joinRuleToIconSrc(Icons, joinRule, space || false) ?? Icons.Hash}
{...props}
ref={ref}
/>
)); ));
@@ -17,7 +17,6 @@ export const SequenceCard = as<
firstChild, firstChild,
lastChild, lastChild,
outlined, outlined,
mergeBorder,
...props ...props
}, },
ref ref
@@ -25,7 +24,7 @@ export const SequenceCard = as<
<Box <Box
as={AsSequenceCard} as={AsSequenceCard}
className={classNames( className={classNames(
css.SequenceCard({ radii, outlined, mergeBorder }), css.SequenceCard({ radii, outlined }),
ContainerColor({ variant }), ContainerColor({ variant }),
className className
)} )}
+2 -11
View File
@@ -11,7 +11,7 @@ export const SequenceCard = recipe({
}, },
borderStyle: 'solid', borderStyle: 'solid',
borderWidth: outlinedWidth, borderWidth: outlinedWidth,
borderBottomWidth: 0,
selectors: { selectors: {
'&:first-child, :not(&) + &': { '&:first-child, :not(&) + &': {
borderTopLeftRadius: [radii], borderTopLeftRadius: [radii],
@@ -20,6 +20,7 @@ export const SequenceCard = recipe({
'&:last-child, &:not(:has(+&))': { '&:last-child, &:not(:has(+&))': {
borderBottomLeftRadius: [radii], borderBottomLeftRadius: [radii],
borderBottomRightRadius: [radii], borderBottomRightRadius: [radii],
borderBottomWidth: outlinedWidth,
}, },
[`&[data-first-child="true"]`]: { [`&[data-first-child="true"]`]: {
borderTopLeftRadius: [radii], borderTopLeftRadius: [radii],
@@ -73,16 +74,6 @@ export const SequenceCard = recipe({
}, },
}, },
}, },
mergeBorder: {
true: {
borderBottomWidth: 0,
selectors: {
'&:last-child, &:not(:has(+&))': {
borderBottomWidth: outlinedWidth,
},
},
},
},
}, },
defaultVariants: { defaultVariants: {
radii: '400', radii: '400',
@@ -1,11 +1,13 @@
import { createVar, style } from '@vanilla-extract/css'; import { createVar, style } from '@vanilla-extract/css';
import { recipe, RecipeVariants } from '@vanilla-extract/recipes'; import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
import { color, config, DefaultReset, Disabled, FocusOutline, toRem } from 'folds'; import { color, config, DefaultReset, Disabled, FocusOutline, toRem } from 'folds';
import { ContainerColor } from '../../styles/ContainerColor.css';
export const Sidebar = style([ export const Sidebar = style([
DefaultReset, DefaultReset,
{ {
width: toRem(66), width: toRem(66),
backgroundColor: color.Background.Container,
borderRight: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`, borderRight: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`,
display: 'flex', display: 'flex',
@@ -185,6 +187,7 @@ export type SidebarAvatarVariants = RecipeVariants<typeof SidebarAvatar>;
export const SidebarFolder = recipe({ export const SidebarFolder = recipe({
base: [ base: [
ContainerColor({ variant: 'Background' }),
{ {
padding: config.space.S100, padding: config.space.S100,
width: toRem(42), width: toRem(42),
@@ -3,6 +3,7 @@ import { color, config } from 'folds';
export const SplashScreen = style({ export const SplashScreen = style({
minHeight: '100%', minHeight: '100%',
backgroundColor: color.Background.Container,
color: color.Background.OnContainer, color: color.Background.OnContainer,
}); });
@@ -1,18 +0,0 @@
import React from 'react';
import { as, Avatar } from 'folds';
import classNames from 'classnames';
import * as css from './styles.css';
type StackedAvatarProps = {
radii?: '0' | '300' | '400' | '500' | 'Pill' | 'Inherit' | undefined;
};
export const StackedAvatar = as<'span', css.StackedAvatarVariants & StackedAvatarProps>(
({ size, variant, className, ...props }, ref) => (
<Avatar
size={size}
className={classNames(css.StackedAvatar({ size, variant }), className)}
{...props}
ref={ref}
/>
)
);
@@ -1 +0,0 @@
export * from './StackedAvatar';
@@ -1,59 +0,0 @@
import { ComplexStyleRule } from '@vanilla-extract/css';
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
import { color, config, ContainerColor, toRem } from 'folds';
const getVariant = (variant: ContainerColor): ComplexStyleRule => ({
outlineColor: color[variant].Container,
});
export const StackedAvatar = recipe({
base: {
backgroundColor: color.Surface.Container,
outlineStyle: 'solid',
selectors: {
'&:first-child': {
marginLeft: 0,
},
'button&': {
cursor: 'pointer',
},
},
},
variants: {
size: {
'200': {
marginLeft: toRem(-6),
outlineWidth: config.borderWidth.B300,
},
'300': {
marginLeft: toRem(-9),
outlineWidth: config.borderWidth.B400,
},
'400': {
marginLeft: toRem(-10.5),
outlineWidth: config.borderWidth.B500,
},
'500': {
marginLeft: toRem(-13),
outlineWidth: config.borderWidth.B600,
},
},
variant: {
Background: getVariant('Background'),
Surface: getVariant('Surface'),
SurfaceVariant: getVariant('SurfaceVariant'),
Primary: getVariant('Primary'),
Secondary: getVariant('Secondary'),
Success: getVariant('Success'),
Warning: getVariant('Warning'),
Critical: getVariant('Critical'),
},
},
defaultVariants: {
size: '400',
variant: 'Surface',
},
});
export type StackedAvatarVariants = RecipeVariants<typeof StackedAvatar>;
+2 -7
View File
@@ -26,12 +26,7 @@ export function SSOStage({
useEffect(() => { useEffect(() => {
const handleMessage = (evt: MessageEvent) => { const handleMessage = (evt: MessageEvent) => {
if ( if (ssoWindow && evt.data === 'authDone' && evt.source === ssoWindow) {
evt.origin === new URL(ssoRedirectURL).origin &&
ssoWindow &&
evt.data === 'authDone' &&
evt.source === ssoWindow
) {
ssoWindow.close(); ssoWindow.close();
setSSOWindow(undefined); setSSOWindow(undefined);
handleSubmit(); handleSubmit();
@@ -42,7 +37,7 @@ export function SSOStage({
return () => { return () => {
window.removeEventListener('message', handleMessage); window.removeEventListener('message', handleMessage);
}; };
}, [ssoWindow, handleSubmit, ssoRedirectURL]); }, [ssoWindow, handleSubmit]);
return ( return (
<Dialog> <Dialog>
@@ -30,15 +30,7 @@ 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( const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication, 256, 256, 'scale', false);
mx,
prev['og:image'] || '',
useAuthentication,
256,
256,
'scale',
false
);
return ( return (
<> <>
@@ -50,7 +42,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
as="a" as="a"
href={url} href={url}
target="_blank" target="_blank"
rel="noreferrer" rel="no-referrer"
size="T200" size="T200"
priority="300" priority="300"
> >
@@ -323,7 +323,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) {
)} )}
/> />
) : ( ) : (
<RoomIcon size="100" joinRule={room.getJoinRule()} roomType={room.getType()} /> <RoomIcon size="100" joinRule={room.getJoinRule()} />
)} )}
</Avatar> </Avatar>
} }
-10
View File
@@ -20,16 +20,6 @@ export type AutoDiscoveryInfo = Record<string, unknown> & {
'm.identity_server'?: { 'm.identity_server'?: {
base_url: string; base_url: string;
}; };
'org.matrix.msc2965.authentication'?: {
account?: string;
issuer?: string;
};
'org.matrix.msc4143.rtc_foci'?: [
{
livekit_service_url: string;
type: 'livekit';
}
];
}; };
export const autoDiscovery = async ( export const autoDiscovery = async (
@@ -291,11 +291,7 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM
)} )}
/> />
) : ( ) : (
<RoomIcon <RoomIcon size="200" joinRule={room.getJoinRule()} />
size="200"
joinRule={room.getJoinRule()}
roomType={room.getType()}
/>
)} )}
</Avatar> </Avatar>
} }
@@ -1,226 +0,0 @@
import { Box, Chip, Icon, IconButton, Icons, Spinner, Text, Tooltip, TooltipProvider } from 'folds';
import React, { useCallback } from 'react';
import { useSetAtom } from 'jotai';
import { StatusDivider } from './components';
import { CallEmbed, useCallControlState } from '../../plugins/call';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { callEmbedAtom } from '../../state/callEmbed';
type MicrophoneButtonProps = {
enabled: boolean;
onToggle: () => Promise<unknown>;
disabled?: boolean;
};
function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps) {
return (
<TooltipProvider
position="Top"
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Surface' : 'Warning'}
fill="Soft"
radii="300"
size="300"
onClick={() => onToggle()}
outlined
disabled={disabled}
>
<Icon size="100" src={enabled ? Icons.Mic : Icons.MicMute} filled={!enabled} />
</IconButton>
)}
</TooltipProvider>
);
}
type SoundButtonProps = {
enabled: boolean;
onToggle: () => void;
disabled?: boolean;
};
function SoundButton({ enabled, onToggle, disabled }: SoundButtonProps) {
return (
<TooltipProvider
position="Top"
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Turn Off Sound' : 'Turn On Sound'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Surface' : 'Warning'}
fill="Soft"
radii="300"
size="300"
onClick={() => onToggle()}
outlined
disabled={disabled}
>
<Icon
size="100"
src={enabled ? Icons.Headphone : Icons.HeadphoneMute}
filled={!enabled}
/>
</IconButton>
)}
</TooltipProvider>
);
}
type VideoButtonProps = {
enabled: boolean;
onToggle: () => Promise<unknown>;
disabled?: boolean;
};
function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
return (
<TooltipProvider
position="Top"
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Stop Camera' : 'Start Camera'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Success' : 'Surface'}
fill="Soft"
radii="300"
size="300"
onClick={() => onToggle()}
outlined
disabled={disabled}
>
<Icon
size="100"
src={enabled ? Icons.VideoCamera : Icons.VideoCameraMute}
filled={enabled}
/>
</IconButton>
)}
</TooltipProvider>
);
}
type ScreenShareButtonProps = {
enabled: boolean;
onToggle: () => void;
disabled?: boolean;
};
function ScreenShareButton({ enabled, onToggle, disabled }: ScreenShareButtonProps) {
return (
<TooltipProvider
position="Top"
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Stop Screenshare' : 'Start Screenshare'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Success' : 'Surface'}
fill="Soft"
radii="300"
size="300"
onClick={onToggle}
outlined
disabled={disabled}
>
<Icon size="100" src={Icons.ScreenShare} filled={enabled} />
</IconButton>
)}
</TooltipProvider>
);
}
export function CallControl({
callEmbed,
compact,
callJoined,
}: {
callEmbed: CallEmbed;
compact: boolean;
callJoined: boolean;
}) {
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
const setCallEmbed = useSetAtom(callEmbedAtom);
const [hangupState, hangup] = useAsyncCallback(
useCallback(() => callEmbed.hangup(), [callEmbed])
);
const exiting =
hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success;
const handleHangup = () => {
if (!callJoined) {
setCallEmbed(undefined);
return;
}
hangup();
};
return (
<Box shrink="No" alignItems="Center" gap="300">
<Box alignItems="Inherit" gap="200">
<MicrophoneButton
enabled={microphone}
onToggle={() => callEmbed.control.toggleMicrophone()}
disabled={!callJoined}
/>
<SoundButton
enabled={sound}
onToggle={() => callEmbed.control.toggleSound()}
disabled={!callJoined}
/>
{!compact && <StatusDivider />}
<VideoButton
enabled={video}
onToggle={() => callEmbed.control.toggleVideo()}
disabled={!callJoined}
/>
{!compact && (
<ScreenShareButton
enabled={screenshare}
onToggle={() => callEmbed.control.toggleScreenshare()}
disabled={!callJoined}
/>
)}
</Box>
<StatusDivider />
<Chip
variant="Critical"
radii="Pill"
fill="Soft"
before={
exiting ? (
<Spinner variant="Critical" fill="Soft" size="50" />
) : (
<Icon size="50" src={Icons.PhoneDown} filled />
)
}
disabled={exiting}
outlined
onClick={handleHangup}
>
{!compact && (
<Text as="span" size="L400">
End
</Text>
)}
</Chip>
</Box>
);
}
@@ -1,56 +0,0 @@
import React from 'react';
import { Room } from 'matrix-js-sdk';
import { Chip, Text } from 'folds';
import { useAtomValue } from 'jotai';
import { useRoomName } from '../../hooks/useRoomMeta';
import { RoomIcon } from '../../components/room-avatar';
import { roomToParentsAtom } from '../../state/room/roomToParents';
import { getAllParents, guessPerfectParent } from '../../utils/room';
import { useOrphanSpaces } from '../../state/hooks/roomList';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { allRoomsAtom } from '../../state/room-list/roomList';
import { mDirectAtom } from '../../state/mDirectList';
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
type CallRoomNameProps = {
room: Room;
};
export function CallRoomName({ room }: CallRoomNameProps) {
const mx = useMatrixClient();
const name = useRoomName(room);
const roomToParents = useAtomValue(roomToParentsAtom);
const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, roomToParents);
const mDirects = useAtomValue(mDirectAtom);
const dm = mDirects.has(room.roomId);
const allRoomsSet = useAllJoinedRoomsSet();
const getRoom = useGetRoom(allRoomsSet);
const allParents = getAllParents(roomToParents, room.roomId);
const orphanParents = allParents && orphanSpaces.filter((o) => allParents.has(o));
const perfectOrphanParent = orphanParents && guessPerfectParent(mx, room.roomId, orphanParents);
const { navigateRoom } = useRoomNavigate();
return (
<Chip
variant="Background"
fill="None"
radii="Pill"
before={
<RoomIcon size="200" joinRule={room.getJoinRule()} roomType={room.getType()} filled />
}
onClick={() => navigateRoom(room.roomId)}
>
<Text size="L400" truncate>
{name}
{!dm && perfectOrphanParent && (
<Text as="span" size="T200" priority="300">
{' •'} <b>{getRoom(perfectOrphanParent)?.name ?? perfectOrphanParent}</b>
</Text>
)}
</Text>
</Chip>
);
}
@@ -1,79 +0,0 @@
import React from 'react';
import { Box, Spinner } from 'folds';
import { LiveChip } from './LiveChip';
import * as css from './styles.css';
import { CallRoomName } from './CallRoomName';
import { CallControl } from './CallControl';
import { useCallMembers, useCallSession } from '../../hooks/useCall';
import { ScreenSize, useScreenSize } from '../../hooks/useScreenSize';
import { MemberGlance } from './MemberGlance';
import { StatusDivider } from './components';
import { CallEmbed } from '../../plugins/call/CallEmbed';
import { useCallJoined } from '../../hooks/useCallEmbed';
import { useCallSpeakers } from '../../hooks/useCallSpeakers';
import { MemberSpeaking } from './MemberSpeaking';
type CallStatusProps = {
callEmbed: CallEmbed;
};
export function CallStatus({ callEmbed }: CallStatusProps) {
const { room } = callEmbed;
const callSession = useCallSession(room);
const callMembers = useCallMembers(room, callSession);
const screenSize = useScreenSize();
const callJoined = useCallJoined(callEmbed);
const speakers = useCallSpeakers(callEmbed);
const compact = screenSize === ScreenSize.Mobile;
const memberVisible = callJoined && callMembers.length > 0;
return (
<Box
className={css.CallStatus}
shrink="No"
gap="400"
alignItems={compact ? undefined : 'Center'}
direction={compact ? 'Column' : 'Row'}
>
<Box grow="Yes" alignItems="Center" gap="200">
{memberVisible ? (
<Box shrink="No">
<LiveChip count={callMembers.length} room={room} members={callMembers} />
</Box>
) : (
<Spinner variant="Secondary" size="200" />
)}
<Box grow="Yes" alignItems="Center" gap="Inherit">
{!compact && (
<>
<CallRoomName room={room} />
{speakers.size > 0 && (
<>
<StatusDivider />
<span data-spacing-node />
<MemberSpeaking room={room} speakers={speakers} />
</>
)}
</>
)}
</Box>
{memberVisible && (
<Box shrink="No">
<MemberGlance room={room} members={callMembers} speakers={speakers} />
</Box>
)}
</Box>
{memberVisible && !compact && <StatusDivider />}
<Box shrink="No" alignItems="Center" gap="Inherit">
{compact && (
<Box grow="Yes">
<CallRoomName room={room} />
</Box>
)}
<CallControl callJoined={callJoined} compact={compact} callEmbed={callEmbed} />
</Box>
</Box>
);
}
-137
View File
@@ -1,137 +0,0 @@
import React, { MouseEventHandler, useState } from 'react';
import {
Avatar,
Badge,
Box,
Chip,
config,
Icon,
Icons,
Menu,
MenuItem,
PopOut,
RectCords,
Scroll,
Text,
toRem,
} from 'folds';
import { CallMembership } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
import FocusTrap from 'focus-trap-react';
import { Room } from 'matrix-js-sdk';
import * as css from './styles.css';
import { stopPropagation } from '../../utils/keyboard';
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { UserAvatar } from '../../components/user-avatar';
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { getMouseEventCords } from '../../utils/dom';
type LiveChipProps = {
room: Room;
members: CallMembership[];
count: number;
};
export function LiveChip({ count, room, members }: LiveChipProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const openUserProfile = useOpenUserRoomProfile();
const [cords, setCords] = useState<RectCords>();
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setCords(evt.currentTarget.getBoundingClientRect());
};
return (
<PopOut
anchor={cords}
position="Top"
align="Start"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setCords(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<Menu
style={{
maxHeight: '75vh',
maxWidth: toRem(300),
display: 'flex',
}}
>
<Box grow="Yes">
<Scroll size="0" hideTrack visibility="Hover">
<Box direction="Column" style={{ padding: config.space.S100 }}>
{members.map((callMember) => {
const userId = callMember.sender;
if (!userId) return null;
const name =
getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
const avatarMxc = getMemberAvatarMxc(room, userId);
const avatarUrl = avatarMxc
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96) ?? undefined
: undefined;
return (
<MenuItem
key={callMember.membershipID}
size="400"
variant="Surface"
radii="300"
style={{ paddingLeft: config.space.S200 }}
onClick={(evt) =>
openUserProfile(
room.roomId,
undefined,
userId,
getMouseEventCords(evt.nativeEvent),
'Right'
)
}
before={
<Avatar size="200" radii="400">
<UserAvatar
userId={userId}
src={avatarUrl}
alt={name}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</Avatar>
}
>
<Text size="T300" truncate>
{name}
</Text>
</MenuItem>
);
})}
</Box>
</Scroll>
</Box>
</Menu>
</FocusTrap>
}
>
<Chip
variant="Surface"
fill="Soft"
before={<Badge variant="Critical" fill="Solid" size="200" />}
after={<Icon size="50" src={cords ? Icons.ChevronBottom : Icons.ChevronTop} />}
radii="Pill"
onClick={handleOpenMenu}
>
<Text className={css.LiveChipText} as="span" size="L400" truncate>
{count} Live
</Text>
</Chip>
</PopOut>
);
}
@@ -1,75 +0,0 @@
import { Box, config, Icon, Icons, Text } from 'folds';
import { CallMembership } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
import React from 'react';
import { Room } from 'matrix-js-sdk';
import { UserAvatar } from '../../components/user-avatar';
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { StackedAvatar } from '../../components/stacked-avatar';
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { getMouseEventCords } from '../../utils/dom';
import * as css from './styles.css';
type MemberGlanceProps = {
room: Room;
members: CallMembership[];
speakers: Set<string>;
max?: number;
};
export function MemberGlance({ room, members, speakers, max = 6 }: MemberGlanceProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const openUserProfile = useOpenUserRoomProfile();
const visibleMembers = members.slice(0, max);
const remainingCount = max && members.length > max ? members.length - max : 0;
return (
<Box alignItems="Center">
{visibleMembers.map((callMember) => {
const userId = callMember.sender;
if (!userId) return null;
const name = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
const avatarMxc = getMemberAvatarMxc(room, userId);
const avatarUrl = avatarMxc
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96) ?? undefined
: undefined;
return (
<StackedAvatar
key={callMember.membershipID}
className={speakers.has(callMember.sender) ? css.SpeakerAvatarOutline : undefined}
title={name}
as="button"
variant="Background"
size="200"
radii="Pill"
onClick={(evt) =>
openUserProfile(
room.roomId,
undefined,
userId,
getMouseEventCords(evt.nativeEvent),
'Top'
)
}
>
<UserAvatar
userId={userId}
src={avatarUrl}
alt={name}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</StackedAvatar>
);
})}
{remainingCount > 0 && (
<Text size="L400" style={{ paddingLeft: config.space.S100 }}>
+{remainingCount}
</Text>
)}
</Box>
);
}
@@ -1,78 +0,0 @@
import { Room } from 'matrix-js-sdk';
import React from 'react';
import { Box, Icon, Icons, Text } from 'folds';
import { getMemberDisplayName } from '../../utils/room';
import { getMxIdLocalPart } from '../../utils/matrix';
type MemberSpeakingProps = {
room: Room;
speakers: Set<string>;
};
export function MemberSpeaking({ room, speakers }: MemberSpeakingProps) {
const speakingNames = Array.from(speakers).map(
(userId) => getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId
);
return (
<Box alignItems="Center" gap="100">
<Icon size="100" src={Icons.Mic} filled />
<Text size="T200" truncate>
{speakingNames.length === 1 && (
<>
<b>{speakingNames[0]}</b>
<Text as="span" size="Inherit" priority="300">
{' is speaking...'}
</Text>
</>
)}
{speakingNames.length === 2 && (
<>
<b>{speakingNames[0]}</b>
<Text as="span" size="Inherit" priority="300">
{' and '}
</Text>
<b>{speakingNames[1]}</b>
<Text as="span" size="Inherit" priority="300">
{' are speaking...'}
</Text>
</>
)}
{speakingNames.length === 3 && (
<>
<b>{speakingNames[0]}</b>
<Text as="span" size="Inherit" priority="300">
{', '}
</Text>
<b>{speakingNames[1]}</b>
<Text as="span" size="Inherit" priority="300">
{' and '}
</Text>
<b>{speakingNames[2]}</b>
<Text as="span" size="Inherit" priority="300">
{' are speaking...'}
</Text>
</>
)}
{speakingNames.length > 3 && (
<>
<b>{speakingNames[0]}</b>
<Text as="span" size="Inherit" priority="300">
{', '}
</Text>
<b>{speakingNames[1]}</b>
<Text as="span" size="Inherit" priority="300">
{', '}
</Text>
<b>{speakingNames[2]}</b>
<Text as="span" size="Inherit" priority="300">
{' and '}
</Text>
<b>{speakingNames.length - 3} others</b>
<Text as="span" size="Inherit" priority="300">
{' are speaking...'}
</Text>
</>
)}
</Text>
</Box>
);
}
@@ -1,9 +0,0 @@
import React from 'react';
import { Line } from 'folds';
import * as css from './styles.css';
export function StatusDivider() {
return (
<Line variant="Background" size="300" direction="Vertical" className={css.ControlDivider} />
);
}
-1
View File
@@ -1 +0,0 @@
export * from './CallStatus';
@@ -1,21 +0,0 @@
import { style } from '@vanilla-extract/css';
import { color, config, toRem } from 'folds';
export const LiveChipText = style({
color: color.Critical.Main,
});
export const CallStatus = style([
{
padding: `${toRem(6)} ${config.space.S200}`,
borderTop: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`,
},
]);
export const ControlDivider = style({
height: toRem(16),
});
export const SpeakerAvatarOutline = style({
boxShadow: `0 0 0 ${config.borderWidth.B600} ${color.Success.Main}`,
});
-203
View File
@@ -1,203 +0,0 @@
import React, { MouseEventHandler, useCallback, useRef, useState } from 'react';
import {
Box,
Button,
config,
Icon,
IconButton,
Icons,
Menu,
MenuItem,
PopOut,
RectCords,
Spinner,
Text,
toRem,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { SequenceCard } from '../../components/sequence-card';
import * as css from './styles.css';
import {
ChatButton,
ControlDivider,
MicrophoneButton,
ScreenShareButton,
SoundButton,
VideoButton,
} from './Controls';
import { CallEmbed, useCallControlState } from '../../plugins/call';
import { useResizeObserver } from '../../hooks/useResizeObserver';
import { stopPropagation } from '../../utils/keyboard';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
type CallControlsProps = {
callEmbed: CallEmbed;
};
export function CallControls({ callEmbed }: CallControlsProps) {
const controlRef = useRef<HTMLDivElement>(null);
const [compact, setCompact] = useState(document.body.clientWidth < 500);
useResizeObserver(
useCallback(() => {
const element = controlRef.current;
if (!element) return;
setCompact(element.clientWidth < 500);
}, []),
useCallback(() => controlRef.current, [])
);
const { microphone, video, sound, screenshare, spotlight } = useCallControlState(
callEmbed.control
);
const [cords, setCords] = useState<RectCords>();
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setCords(evt.currentTarget.getBoundingClientRect());
};
const handleSpotlightClick = () => {
callEmbed.control.toggleSpotlight();
setCords(undefined);
};
const handleReactionsClick = () => {
callEmbed.control.toggleReactions();
setCords(undefined);
};
const handleSettingsClick = () => {
callEmbed.control.toggleSettings();
setCords(undefined);
};
const [hangupState, hangup] = useAsyncCallback(
useCallback(() => callEmbed.hangup(), [callEmbed])
);
const exiting =
hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success;
return (
<Box
ref={controlRef}
className={css.CallControlContainer}
justifyContent="Center"
alignItems="Center"
>
<SequenceCard
className={css.ControlCard}
variant="SurfaceVariant"
gap="400"
radii="500"
alignItems="Center"
justifyContent="SpaceBetween"
>
<Box alignItems="Center" gap="Inherit" grow="Yes" direction={compact ? 'Column' : 'Row'}>
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
<MicrophoneButton
enabled={microphone}
onToggle={() => callEmbed.control.toggleMicrophone()}
/>
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
</Box>
{!compact && <ControlDivider />}
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
<ScreenShareButton
enabled={screenshare}
onToggle={() => callEmbed.control.toggleScreenshare()}
/>
</Box>
</Box>
{!compact && <ControlDivider />}
<Box alignItems="Center" gap="Inherit" grow="Yes" direction={compact ? 'Column' : 'Row'}>
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
<ChatButton />
<PopOut
anchor={cords}
position="Top"
align="Center"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setCords(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<Menu>
<Box direction="Column" style={{ padding: config.space.S100 }}>
<MenuItem
size="300"
variant="Surface"
radii="300"
onClick={handleSpotlightClick}
>
<Text size="B300" truncate>
{spotlight ? 'Grid View' : 'Spotlight View'}
</Text>
</MenuItem>
<MenuItem
size="300"
variant="Surface"
radii="300"
onClick={handleReactionsClick}
>
<Text size="B300" truncate>
Reactions
</Text>
</MenuItem>
<MenuItem
size="300"
variant="Surface"
radii="300"
onClick={handleSettingsClick}
>
<Text size="B300" truncate>
Settings
</Text>
</MenuItem>
</Box>
</Menu>
</FocusTrap>
}
>
<IconButton
variant="Surface"
fill="Soft"
radii="400"
size="400"
onClick={handleOpenMenu}
outlined
aria-pressed={!!cords}
>
<Icon size="400" src={Icons.VerticalDots} />
</IconButton>
</PopOut>
</Box>
<Box shrink="No" direction="Column">
<Button
style={{ minWidth: toRem(88) }}
variant="Critical"
fill="Solid"
onClick={hangup}
before={
exiting ? (
<Spinner variant="Critical" fill="Solid" size="200" />
) : (
<Icon src={Icons.PhoneDown} size="200" filled />
)
}
disabled={exiting}
>
<Text size="B400">End</Text>
</Button>
</Box>
</Box>
</SequenceCard>
</Box>
);
}
-121
View File
@@ -1,121 +0,0 @@
import { CallMembership, SessionMembershipData } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
import React, { useState } from 'react';
import { Avatar, Box, Icon, Icons, Text } from 'folds';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { SequenceCard } from '../../components/sequence-card';
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
import { useRoom } from '../../hooks/useRoom';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { UserAvatar } from '../../components/user-avatar';
import { getMouseEventCords } from '../../utils/dom';
import * as css from './styles.css';
interface MemberWithMembershipData {
membershipData?: SessionMembershipData & {
'm.call.intent': 'video' | 'audio';
};
}
type CallMemberCardProps = {
member: CallMembership;
};
export function CallMemberCard({ member }: CallMemberCardProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const room = useRoom();
const openUserProfile = useOpenUserRoomProfile();
const userId = member.sender;
if (!userId) return null;
const name = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
const avatarMxc = getMemberAvatarMxc(room, userId);
const avatarUrl = avatarMxc
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96) ?? undefined
: undefined;
const audioOnly =
(member as unknown as MemberWithMembershipData).membershipData?.['m.call.intent'] === 'audio';
return (
<SequenceCard
as="button"
key={member.membershipID}
className={css.CallMemberCard}
variant="SurfaceVariant"
radii="500"
onClick={(evt: any) =>
openUserProfile(
room.roomId,
undefined,
userId,
getMouseEventCords(evt.nativeEvent),
'Right'
)
}
>
<Box grow="Yes" gap="300" alignItems="Center">
<Avatar size="200" radii="400">
<UserAvatar
userId={userId}
src={avatarUrl}
alt={name}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</Avatar>
<Box grow="Yes">
<Text size="L400" truncate>
{name}
</Text>
</Box>
{audioOnly && <Icon src={Icons.VideoCameraMute} size="100" />}
</Box>
</SequenceCard>
);
}
export function CallMemberRenderer({
members,
max = 4,
}: {
members: CallMembership[];
max?: number;
}) {
const [viewMore, setViewMore] = useState(false);
const truncatedMembers = viewMore ? members : members.slice(0, 4);
const remaining = members.length - truncatedMembers.length;
return (
<>
{truncatedMembers.map((member) => (
<CallMemberCard key={member.membershipID} member={member} />
))}
{members.length > max && (
<SequenceCard
as="button"
className={css.CallMemberCard}
variant="SurfaceVariant"
radii="500"
onClick={() => setViewMore(!viewMore)}
>
<Box grow="Yes" gap="300" alignItems="Center">
{viewMore ? (
<Text size="L400" truncate>
Collapse
</Text>
) : (
<Text size="L400" truncate>
{remaining === 0 ? `+${remaining} Other` : `+${remaining} Others`}
</Text>
)}
</Box>
<Icon src={viewMore ? Icons.ChevronTop : Icons.ChevronBottom} size="100" />
</SequenceCard>
)}
</>
);
}
-145
View File
@@ -1,145 +0,0 @@
import React, { RefObject, useRef } from 'react';
import { Badge, Box, color, Header, Scroll, Text, toRem } from 'folds';
import { useCallEmbed, useCallJoined, useCallEmbedPlacementSync } from '../../hooks/useCallEmbed';
import { PrescreenControls } from './PrescreenControls';
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { useRoom } from '../../hooks/useRoom';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { StateEvent } from '../../../types/matrix/room';
import { useCallMembers, useCallSession } from '../../hooks/useCall';
import { CallMemberRenderer } from './CallMemberCard';
import * as css from './styles.css';
import { CallControls } from './CallControls';
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
function LivekitServerMissingMessage() {
return (
<Text style={{ margin: 'auto', color: color.Critical.Main }} size="L400" align="Center">
Your homeserver does not support calling. But you can still join call started by others.
</Text>
);
}
function JoinMessage({
hasParticipant,
livekitSupported,
}: {
hasParticipant?: boolean;
livekitSupported?: boolean;
}) {
if (hasParticipant) return null;
if (livekitSupported === false) {
return <LivekitServerMissingMessage />;
}
return (
<Text style={{ margin: 'auto' }} size="L400" align="Center">
Voice chats empty Be the first to hop in!
</Text>
);
}
function NoPermissionMessage() {
return (
<Text style={{ margin: 'auto' }} size="L400" align="Center">
You don&#39;t have permission to join!
</Text>
);
}
function AlreadyInCallMessage() {
return (
<Text style={{ margin: 'auto', color: color.Warning.Main }} size="L400" align="Center">
Already in another call End the current call to join!
</Text>
);
}
function CallPrescreen() {
const mx = useMatrixClient();
const room = useRoom();
const livekitSupported = useLivekitSupport();
const powerLevels = usePowerLevelsContext();
const creators = useRoomCreators(room);
const permissions = useRoomPermissions(creators, powerLevels);
const hasPermission = permissions.event(StateEvent.GroupCallMemberPrefix, mx.getSafeUserId());
const callSession = useCallSession(room);
const callMembers = useCallMembers(room, callSession);
const hasParticipant = callMembers.length > 0;
const callEmbed = useCallEmbed();
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
const canJoin = hasPermission && (livekitSupported || hasParticipant);
return (
<Scroll variant="Surface" hideTrack>
<Box className={css.CallViewContent} alignItems="Center" justifyContent="Center">
<Box style={{ maxWidth: toRem(382), width: '100%' }} direction="Column" gap="100">
{hasParticipant && (
<Header size="300">
<Box grow="Yes" alignItems="Center">
<Text size="L400">Participant</Text>
</Box>
<Badge variant="Critical" fill="Solid" size="400">
<Text as="span" size="L400" truncate>
{callMembers.length} Live
</Text>
</Badge>
</Header>
)}
<CallMemberRenderer members={callMembers} />
<PrescreenControls canJoin={canJoin} />
<Box className={css.PrescreenMessage} alignItems="Center">
{!inOtherCall &&
(hasPermission ? (
<JoinMessage hasParticipant={hasParticipant} livekitSupported={livekitSupported} />
) : (
<NoPermissionMessage />
))}
{inOtherCall && <AlreadyInCallMessage />}
</Box>
</Box>
</Box>
</Scroll>
);
}
type CallJoinedProps = {
containerRef: RefObject<HTMLDivElement>;
joined: boolean;
};
function CallJoined({ joined, containerRef }: CallJoinedProps) {
const callEmbed = useCallEmbed();
return (
<Box grow="Yes" direction="Column">
<Box grow="Yes" ref={containerRef} />
{callEmbed && joined && <CallControls callEmbed={callEmbed} />}
</Box>
);
}
export function CallView() {
const room = useRoom();
const callContainerRef = useRef<HTMLDivElement>(null);
useCallEmbedPlacementSync(callContainerRef);
const callEmbed = useCallEmbed();
const callJoined = useCallJoined(callEmbed);
const currentJoined = callEmbed?.roomId === room.roomId && callJoined;
return (
<Box style={{ minWidth: toRem(280) }} grow="Yes">
{!currentJoined && <CallPrescreen />}
<CallJoined joined={currentJoined} containerRef={callContainerRef} />
</Box>
);
}
-177
View File
@@ -1,177 +0,0 @@
import React from 'react';
import { Icon, IconButton, Icons, Line, Text, Tooltip, TooltipProvider } from 'folds';
import { useAtom } from 'jotai';
import * as css from './styles.css';
import { callChatAtom } from '../../state/callEmbed';
export function ControlDivider() {
return (
<Line variant="SurfaceVariant" size="300" direction="Vertical" className={css.ControlDivider} />
);
}
type MicrophoneButtonProps = {
enabled: boolean;
onToggle: () => void;
};
export function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
return (
<TooltipProvider
position="Top"
delay={500}
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Surface' : 'Warning'}
fill="Soft"
radii="400"
size="400"
onClick={() => onToggle()}
outlined
>
<Icon size="400" src={enabled ? Icons.Mic : Icons.MicMute} filled={!enabled} />
</IconButton>
)}
</TooltipProvider>
);
}
type SoundButtonProps = {
enabled: boolean;
onToggle: () => void;
};
export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
return (
<TooltipProvider
position="Top"
delay={500}
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Turn Off Sound' : 'Turn On Sound'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Surface' : 'Warning'}
fill="Soft"
radii="400"
size="400"
onClick={() => onToggle()}
outlined
>
<Icon
size="400"
src={enabled ? Icons.Headphone : Icons.HeadphoneMute}
filled={!enabled}
/>
</IconButton>
)}
</TooltipProvider>
);
}
type VideoButtonProps = {
enabled: boolean;
onToggle: () => void;
};
export function VideoButton({ enabled, onToggle }: VideoButtonProps) {
return (
<TooltipProvider
position="Top"
delay={500}
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Stop Camera' : 'Start Camera'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Success' : 'Surface'}
fill="Soft"
radii="400"
size="400"
onClick={() => onToggle()}
outlined
>
<Icon
size="400"
src={enabled ? Icons.VideoCamera : Icons.VideoCameraMute}
filled={enabled}
/>
</IconButton>
)}
</TooltipProvider>
);
}
type ScreenShareButtonProps = {
enabled: boolean;
onToggle: () => void;
};
export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
return (
<TooltipProvider
position="Top"
delay={500}
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Stop Screenshare' : 'Start Screenshare'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Success' : 'Surface'}
fill="Soft"
radii="400"
size="400"
onClick={() => onToggle()}
outlined
>
<Icon size="400" src={Icons.ScreenShare} filled={enabled} />
</IconButton>
)}
</TooltipProvider>
);
}
export function ChatButton() {
const [chat, setChat] = useAtom(callChatAtom);
return (
<TooltipProvider
position="Top"
delay={500}
tooltip={
<Tooltip>
<Text size="T200">{chat ? 'Close Chat' : 'Open Chat'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={chat ? 'Success' : 'Surface'}
fill="Soft"
radii="400"
size="400"
onClick={() => setChat(!chat)}
outlined
>
<Icon size="400" src={Icons.Message} filled={chat} />
</IconButton>
)}
</TooltipProvider>
);
}
@@ -1,67 +0,0 @@
import React from 'react';
import { Box, Button, Icon, Icons, Spinner, Text } from 'folds';
import { SequenceCard } from '../../components/sequence-card';
import * as css from './styles.css';
import { ChatButton, ControlDivider, MicrophoneButton, SoundButton, VideoButton } from './Controls';
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
import { useCallEmbed, useCallJoined, useCallStart } from '../../hooks/useCallEmbed';
import { useCallPreferences } from '../../state/hooks/callPreferences';
type PrescreenControlsProps = {
canJoin?: boolean;
};
export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
const room = useRoom();
const callEmbed = useCallEmbed();
const callJoined = useCallJoined(callEmbed);
const direct = useIsDirectRoom();
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
const startCall = useCallStart(direct);
const joining = callEmbed?.roomId === room.roomId && !callJoined;
const disabled = inOtherCall || !canJoin;
const { microphone, video, sound, toggleMicrophone, toggleVideo, toggleSound } =
useCallPreferences();
return (
<SequenceCard
className={css.ControlCard}
variant="SurfaceVariant"
gap="400"
radii="500"
alignItems="Center"
justifyContent="SpaceBetween"
wrap="Wrap"
>
<Box shrink="No" alignItems="Inherit" justifyContent="SpaceBetween" gap="200">
<MicrophoneButton enabled={microphone} onToggle={toggleMicrophone} />
<SoundButton enabled={sound} onToggle={toggleSound} />
</Box>
<ControlDivider />
<Box shrink="No" alignItems="Inherit" justifyContent="SpaceBetween" gap="200">
<VideoButton enabled={video} onToggle={toggleVideo} />
<ChatButton />
</Box>
<Box grow="Yes" direction="Column">
<Button
variant={disabled ? 'Secondary' : 'Success'}
fill={disabled ? 'Soft' : 'Solid'}
onClick={() => startCall(room, { microphone, video, sound })}
disabled={disabled || joining}
before={
joining ? (
<Spinner variant="Success" fill="Solid" size="200" />
) : (
<Icon src={Icons.Phone} size="200" filled />
)
}
>
<Text size="B400">Join</Text>
</Button>
</Box>
</SequenceCard>
);
}
-28
View File
@@ -1,28 +0,0 @@
import { style } from '@vanilla-extract/css';
import { config, toRem } from 'folds';
export const CallViewContent = style({
padding: config.space.S400,
paddingRight: 0,
minHeight: '100%',
});
export const ControlCard = style({
padding: config.space.S300,
});
export const ControlDivider = style({
height: toRem(24),
});
export const CallMemberCard = style({
padding: config.space.S300,
});
export const CallControlContainer = style({
padding: config.space.S400,
});
export const PrescreenMessage = style({
padding: config.space.S200,
});
@@ -6,8 +6,9 @@ import { useAtomValue } from 'jotai';
import { import {
ExtendedJoinRules, ExtendedJoinRules,
JoinRulesSwitcher, JoinRulesSwitcher,
useJoinRuleIcons, useRoomJoinRuleIcon,
useRoomJoinRuleLabel, useRoomJoinRuleLabel,
useSpaceJoinRuleIcon,
} from '../../../components/JoinRulesSwitcher'; } from '../../../components/JoinRulesSwitcher';
import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css'; import { SequenceCardStyle } from '../../room-settings/styles.css';
@@ -74,7 +75,8 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
return r; return r;
}, [allowKnockRestricted, allowRestricted, allowKnock, space]); }, [allowKnockRestricted, allowRestricted, allowKnock, space]);
const icons = useJoinRuleIcons(room.getType()); const icons = useRoomJoinRuleIcon();
const spaceIcons = useSpaceJoinRuleIcon();
const labels = useRoomJoinRuleLabel(); const labels = useRoomJoinRuleLabel();
const [submitState, submit] = useAsyncCallback( const [submitState, submit] = useAsyncCallback(
@@ -135,7 +137,7 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
} }
after={ after={
<JoinRulesSwitcher <JoinRulesSwitcher
icons={icons} icons={room.isSpaceRoom() ? spaceIcons : icons}
labels={labels} labels={labels}
rules={joinRules} rules={joinRules}
value={rule} value={rule}
@@ -199,7 +199,7 @@ export function RoomProfileEdit({
alt={name} alt={name}
renderFallback={() => ( renderFallback={() => (
<RoomIcon <RoomIcon
roomType={room.getType()} space={room.isSpaceRoom()}
size="400" size="400"
joinRule={joinRule?.join_rule ?? JoinRule.Invite} joinRule={joinRule?.join_rule ?? JoinRule.Invite}
filled filled
@@ -342,7 +342,7 @@ export function RoomProfile({ permissions }: RoomProfileProps) {
alt={name} alt={name}
renderFallback={() => ( renderFallback={() => (
<RoomIcon <RoomIcon
roomType={room.getType()} space={room.isSpaceRoom()}
size="400" size="400"
joinRule={joinRule?.join_rule ?? JoinRule.Invite} joinRule={joinRule?.join_rule ?? JoinRule.Invite}
filled filled
@@ -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 { useGetMemberPowerLevel, usePowerLevels } from '../../../hooks/usePowerLevels'; import { 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,13 +87,12 @@ 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, getPowerLevel); const memberPowerSort = useMemberPowerSort(creators);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
+26 -62
View File
@@ -1,5 +1,5 @@
import React, { FormEventHandler, useCallback, useEffect, useState } from 'react'; import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
import { MatrixError, Room, JoinRule } from 'matrix-js-sdk'; import { MatrixError, Room } from 'matrix-js-sdk';
import { import {
Box, Box,
Button, Button,
@@ -33,43 +33,24 @@ import {
createRoom, createRoom,
CreateRoomAliasInput, CreateRoomAliasInput,
CreateRoomData, CreateRoomData,
CreateRoomAccess, CreateRoomKind,
CreateRoomAccessSelector, CreateRoomKindSelector,
RoomVersionSelector, RoomVersionSelector,
useAdditionalCreators, useAdditionalCreators,
CreateRoomType,
} from '../../components/create-room'; } from '../../components/create-room';
import { RoomType } from '../../../types/matrix/room';
import { CreateRoomTypeSelector } from '../../components/create-room/CreateRoomTypeSelector';
import { getRoomIconSrc } from '../../utils/room';
const getCreateRoomAccessToIcon = (access: CreateRoomAccess, type?: CreateRoomType) => { const getCreateRoomKindToIcon = (kind: CreateRoomKind) => {
const isVoiceRoom = type === CreateRoomType.VoiceRoom; if (kind === CreateRoomKind.Private) return Icons.HashLock;
if (kind === CreateRoomKind.Restricted) return Icons.Hash;
let joinRule: JoinRule = JoinRule.Public; return Icons.HashGlobe;
if (access === CreateRoomAccess.Restricted) joinRule = JoinRule.Restricted;
if (access === CreateRoomAccess.Private) joinRule = JoinRule.Knock;
return getRoomIconSrc(Icons, isVoiceRoom ? RoomType.Call : undefined, joinRule);
};
const getCreateRoomTypeToIcon = (type: CreateRoomType) => {
if (type === CreateRoomType.VoiceRoom) return Icons.VolumeHigh;
return Icons.Hash;
}; };
type CreateRoomFormProps = { type CreateRoomFormProps = {
defaultAccess?: CreateRoomAccess; defaultKind?: CreateRoomKind;
defaultType?: CreateRoomType;
space?: Room; space?: Room;
onCreate?: (roomId: string) => void; onCreate?: (roomId: string) => void;
}; };
export function CreateRoomForm({ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormProps) {
defaultAccess,
defaultType,
space,
onCreate,
}: CreateRoomFormProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const alive = useAlive(); const alive = useAlive();
@@ -83,9 +64,8 @@ export function CreateRoomForm({
const allowRestricted = space && restrictedSupported(selectedRoomVersion); const allowRestricted = space && restrictedSupported(selectedRoomVersion);
const [type, setType] = useState(defaultType ?? CreateRoomType.TextRoom); const [kind, setKind] = useState(
const [access, setAccess] = useState( defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private
defaultAccess ?? (allowRestricted ? CreateRoomAccess.Restricted : CreateRoomAccess.Private)
); );
const allowAdditionalCreators = creatorsSupported(selectedRoomVersion); const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } = const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } =
@@ -95,13 +75,13 @@ export function CreateRoomForm({
const [knock, setKnock] = useState(false); const [knock, setKnock] = useState(false);
const [advance, setAdvance] = useState(false); const [advance, setAdvance] = useState(false);
const allowKnock = access === CreateRoomAccess.Private && knockSupported(selectedRoomVersion); const allowKnock = kind === CreateRoomKind.Private && knockSupported(selectedRoomVersion);
const allowKnockRestricted = const allowKnockRestricted =
access === CreateRoomAccess.Restricted && knockRestrictedSupported(selectedRoomVersion); kind === CreateRoomKind.Restricted && knockRestrictedSupported(selectedRoomVersion);
const handleRoomVersionChange = (version: string) => { const handleRoomVersionChange = (version: string) => {
if (!restrictedSupported(version)) { if (!restrictedSupported(version)) {
setAccess(CreateRoomAccess.Private); setKind(CreateRoomKind.Private);
} }
selectRoomVersion(version); selectRoomVersion(version);
}; };
@@ -127,23 +107,19 @@ export function CreateRoomForm({
aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined; aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined;
if (!roomName) return; if (!roomName) return;
const publicRoom = access === CreateRoomAccess.Public; const publicRoom = kind === CreateRoomKind.Public;
let roomKnock = false; let roomKnock = false;
if (allowKnock && access === CreateRoomAccess.Private) { if (allowKnock && kind === CreateRoomKind.Private) {
roomKnock = knock; roomKnock = knock;
} }
if (allowKnockRestricted && access === CreateRoomAccess.Restricted) { if (allowKnockRestricted && kind === CreateRoomKind.Restricted) {
roomKnock = knock; roomKnock = knock;
} }
let roomType: RoomType | undefined;
if (type === CreateRoomType.VoiceRoom) roomType = RoomType.Call;
create({ create({
version: selectedRoomVersion, version: selectedRoomVersion,
type: roomType,
parent: space, parent: space,
access, kind,
name: roomName, name: roomName,
topic: roomTopic || undefined, topic: roomTopic || undefined,
aliasLocalPart: publicRoom ? aliasLocalPart : undefined, aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
@@ -160,32 +136,21 @@ export function CreateRoomForm({
return ( return (
<Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500"> <Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500">
{!space && (
<Box direction="Column" gap="100">
<Text size="L400">Type</Text>
<CreateRoomTypeSelector
value={type}
onSelect={setType}
disabled={disabled}
getIcon={getCreateRoomTypeToIcon}
/>
</Box>
)}
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
<Text size="L400">Access</Text> <Text size="L400">Access</Text>
<CreateRoomAccessSelector <CreateRoomKindSelector
value={access} value={kind}
onSelect={setAccess} onSelect={setKind}
canRestrict={allowRestricted} canRestrict={allowRestricted}
disabled={disabled} disabled={disabled}
getIcon={(roomAccess) => getCreateRoomAccessToIcon(roomAccess, type)} getIcon={getCreateRoomKindToIcon}
/> />
</Box> </Box>
<Box shrink="No" direction="Column" gap="100"> <Box shrink="No" direction="Column" gap="100">
<Text size="L400">Name</Text> <Text size="L400">Name</Text>
<Input <Input
required required
before={<Icon size="100" src={getCreateRoomAccessToIcon(access, type)} />} before={<Icon size="100" src={getCreateRoomKindToIcon(kind)} />}
name="nameInput" name="nameInput"
autoFocus autoFocus
size="500" size="500"
@@ -206,20 +171,19 @@ export function CreateRoomForm({
/> />
</Box> </Box>
{access === CreateRoomAccess.Public && <CreateRoomAliasInput disabled={disabled} />} {kind === CreateRoomKind.Public && <CreateRoomAliasInput disabled={disabled} />}
<Box shrink="No" direction="Column" gap="100"> <Box shrink="No" direction="Column" gap="100">
<Box gap="200" alignItems="End"> <Box gap="200" alignItems="End">
<Text size="L400">Options</Text> <Text size="L400">Options</Text>
<Box grow="Yes" justifyContent="End"> <Box grow="Yes" justifyContent="End">
<Chip <Chip
fill="None"
radii="Pill" radii="Pill"
before={<Icon src={advance ? Icons.ChevronTop : Icons.ChevronBottom} size="50" />} before={<Icon src={advance ? Icons.ChevronTop : Icons.ChevronBottom} size="50" />}
onClick={() => setAdvance(!advance)} onClick={() => setAdvance(!advance)}
type="button" type="button"
> >
<Text size="T200">Advanced Options</Text> <Text size="T200">Advance Options</Text>
</Chip> </Chip>
</Box> </Box>
</Box> </Box>
@@ -237,7 +201,7 @@ export function CreateRoomForm({
/> />
</SequenceCard> </SequenceCard>
)} )}
{access !== CreateRoomAccess.Public && ( {kind !== CreateRoomKind.Public && (
<> <>
<SequenceCard <SequenceCard
style={{ padding: config.space.S300 }} style={{ padding: config.space.S300 }}
@@ -23,13 +23,12 @@ import {
} from '../../state/hooks/createRoomModal'; } from '../../state/hooks/createRoomModal';
import { CreateRoomModalState } from '../../state/createRoomModal'; import { CreateRoomModalState } from '../../state/createRoomModal';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { CreateRoomType } from '../../components/create-room/types';
type CreateRoomModalProps = { type CreateRoomModalProps = {
state: CreateRoomModalState; state: CreateRoomModalState;
}; };
function CreateRoomModal({ state }: CreateRoomModalProps) { function CreateRoomModal({ state }: CreateRoomModalProps) {
const { spaceId, type } = state; const { spaceId } = state;
const closeDialog = useCloseCreateRoomModal(); const closeDialog = useCloseCreateRoomModal();
const allJoinedRooms = useAllJoinedRoomsSet(); const allJoinedRooms = useAllJoinedRoomsSet();
@@ -58,9 +57,7 @@ function CreateRoomModal({ state }: CreateRoomModalProps) {
}} }}
> >
<Box grow="Yes"> <Box grow="Yes">
<Text size="H4"> <Text size="H4">New Room</Text>
{type === CreateRoomType.VoiceRoom ? 'New Voice Room' : 'New Chat Room'}
</Text>
</Box> </Box>
<Box shrink="No"> <Box shrink="No">
<IconButton size="300" radii="300" onClick={closeDialog}> <IconButton size="300" radii="300" onClick={closeDialog}>
@@ -77,7 +74,7 @@ function CreateRoomModal({ state }: CreateRoomModalProps) {
direction="Column" direction="Column"
gap="500" gap="500"
> >
<CreateRoomForm space={space} onCreate={closeDialog} defaultType={type} /> <CreateRoomForm space={space} onCreate={closeDialog} />
</Box> </Box>
</Scroll> </Scroll>
</Box> </Box>
+24 -25
View File
@@ -33,25 +33,25 @@ import {
createRoom, createRoom,
CreateRoomAliasInput, CreateRoomAliasInput,
CreateRoomData, CreateRoomData,
CreateRoomAccess, CreateRoomKind,
CreateRoomAccessSelector, CreateRoomKindSelector,
RoomVersionSelector, RoomVersionSelector,
useAdditionalCreators, useAdditionalCreators,
} from '../../components/create-room'; } from '../../components/create-room';
import { RoomType } from '../../../types/matrix/room'; import { RoomType } from '../../../types/matrix/room';
const getCreateSpaceAccessToIcon = (access: CreateRoomAccess) => { const getCreateSpaceKindToIcon = (kind: CreateRoomKind) => {
if (access === CreateRoomAccess.Private) return Icons.SpaceLock; if (kind === CreateRoomKind.Private) return Icons.SpaceLock;
if (access === CreateRoomAccess.Restricted) return Icons.Space; if (kind === CreateRoomKind.Restricted) return Icons.Space;
return Icons.SpaceGlobe; return Icons.SpaceGlobe;
}; };
type CreateSpaceFormProps = { type CreateSpaceFormProps = {
defaultAccess?: CreateRoomAccess; defaultKind?: CreateRoomKind;
space?: Room; space?: Room;
onCreate?: (roomId: string) => void; onCreate?: (roomId: string) => void;
}; };
export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceFormProps) { export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFormProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const alive = useAlive(); const alive = useAlive();
@@ -65,8 +65,8 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
const allowRestricted = space && restrictedSupported(selectedRoomVersion); const allowRestricted = space && restrictedSupported(selectedRoomVersion);
const [access, setAccess] = useState( const [kind, setKind] = useState(
defaultAccess ?? (allowRestricted ? CreateRoomAccess.Restricted : CreateRoomAccess.Private) defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private
); );
const allowAdditionalCreators = creatorsSupported(selectedRoomVersion); const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
@@ -76,13 +76,13 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
const [knock, setKnock] = useState(false); const [knock, setKnock] = useState(false);
const [advance, setAdvance] = useState(false); const [advance, setAdvance] = useState(false);
const allowKnock = access === CreateRoomAccess.Private && knockSupported(selectedRoomVersion); const allowKnock = kind === CreateRoomKind.Private && knockSupported(selectedRoomVersion);
const allowKnockRestricted = const allowKnockRestricted =
access === CreateRoomAccess.Restricted && knockRestrictedSupported(selectedRoomVersion); kind === CreateRoomKind.Restricted && knockRestrictedSupported(selectedRoomVersion);
const handleRoomVersionChange = (version: string) => { const handleRoomVersionChange = (version: string) => {
if (!restrictedSupported(version)) { if (!restrictedSupported(version)) {
setAccess(CreateRoomAccess.Private); setKind(CreateRoomKind.Private);
} }
selectRoomVersion(version); selectRoomVersion(version);
}; };
@@ -108,12 +108,12 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined; aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined;
if (!roomName) return; if (!roomName) return;
const publicRoom = access === CreateRoomAccess.Public; const publicRoom = kind === CreateRoomKind.Public;
let roomKnock = false; let roomKnock = false;
if (allowKnock && access === CreateRoomAccess.Private) { if (allowKnock && kind === CreateRoomKind.Private) {
roomKnock = knock; roomKnock = knock;
} }
if (allowKnockRestricted && access === CreateRoomAccess.Restricted) { if (allowKnockRestricted && kind === CreateRoomKind.Restricted) {
roomKnock = knock; roomKnock = knock;
} }
@@ -121,7 +121,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
version: selectedRoomVersion, version: selectedRoomVersion,
type: RoomType.Space, type: RoomType.Space,
parent: space, parent: space,
access, kind,
name: roomName, name: roomName,
topic: roomTopic || undefined, topic: roomTopic || undefined,
aliasLocalPart: publicRoom ? aliasLocalPart : undefined, aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
@@ -139,19 +139,19 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
<Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500"> <Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500">
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
<Text size="L400">Access</Text> <Text size="L400">Access</Text>
<CreateRoomAccessSelector <CreateRoomKindSelector
value={access} value={kind}
onSelect={setAccess} onSelect={setKind}
canRestrict={allowRestricted} canRestrict={allowRestricted}
disabled={disabled} disabled={disabled}
getIcon={getCreateSpaceAccessToIcon} getIcon={getCreateSpaceKindToIcon}
/> />
</Box> </Box>
<Box shrink="No" direction="Column" gap="100"> <Box shrink="No" direction="Column" gap="100">
<Text size="L400">Name</Text> <Text size="L400">Name</Text>
<Input <Input
required required
before={<Icon size="100" src={getCreateSpaceAccessToIcon(access)} />} before={<Icon size="100" src={getCreateSpaceKindToIcon(kind)} />}
name="nameInput" name="nameInput"
autoFocus autoFocus
size="500" size="500"
@@ -172,7 +172,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
/> />
</Box> </Box>
{access === CreateRoomAccess.Public && <CreateRoomAliasInput disabled={disabled} />} {kind === CreateRoomKind.Public && <CreateRoomAliasInput disabled={disabled} />}
<Box shrink="No" direction="Column" gap="100"> <Box shrink="No" direction="Column" gap="100">
<Box gap="200" alignItems="End"> <Box gap="200" alignItems="End">
@@ -180,12 +180,11 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
<Box grow="Yes" justifyContent="End"> <Box grow="Yes" justifyContent="End">
<Chip <Chip
radii="Pill" radii="Pill"
fill="None"
before={<Icon src={advance ? Icons.ChevronTop : Icons.ChevronBottom} size="50" />} before={<Icon src={advance ? Icons.ChevronTop : Icons.ChevronBottom} size="50" />}
onClick={() => setAdvance(!advance)} onClick={() => setAdvance(!advance)}
type="button" type="button"
> >
<Text size="T200">Advanced Options</Text> <Text size="T200">Advance Options</Text>
</Chip> </Chip>
</Box> </Box>
</Box> </Box>
@@ -203,7 +202,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
/> />
</SequenceCard> </SequenceCard>
)} )}
{access !== CreateRoomAccess.Public && advance && (allowKnock || allowKnockRestricted) && ( {kind !== CreateRoomKind.Public && advance && (allowKnock || allowKnockRestricted) && (
<SequenceCard <SequenceCard
style={{ padding: config.space.S300 }} style={{ padding: config.space.S300 }}
variant="SurfaceVariant" variant="SurfaceVariant"
@@ -10,7 +10,6 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
import { allRoomsAtom } from '../../state/room-list/roomList'; import { allRoomsAtom } from '../../state/room-list/roomList';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { BackRouteHandler } from '../../components/BackRouteHandler'; import { BackRouteHandler } from '../../components/BackRouteHandler';
import { useTheme } from '../../hooks/useTheme';
type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] }; type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] };
export function JoinBeforeNavigate({ export function JoinBeforeNavigate({
@@ -22,7 +21,6 @@ export function JoinBeforeNavigate({
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
const { navigateRoom, navigateSpace } = useRoomNavigate(); const { navigateRoom, navigateSpace } = useRoomNavigate();
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const theme = useTheme();
const handleView = (roomId: string) => { const handleView = (roomId: string) => {
if (mx.getRoom(roomId)?.isSpaceRoom()) { if (mx.getRoom(roomId)?.isSpaceRoom()) {
@@ -33,7 +31,7 @@ export function JoinBeforeNavigate({
}; };
return ( return (
<Page transparent={theme.flat}> <Page>
<PageHeader balance> <PageHeader balance>
<Box grow="Yes" gap="200"> <Box grow="Yes" gap="200">
<Box shrink="No"> <Box shrink="No">
+1 -3
View File
@@ -56,7 +56,6 @@ import { useGetRoom } from '../../hooks/useGetRoom';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions'; import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators'; import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
import { useTheme } from '../../hooks/useTheme';
const useCanDropLobbyItem = ( const useCanDropLobbyItem = (
space: Room, space: Room,
@@ -152,7 +151,6 @@ const useCanDropLobbyItem = (
export function Lobby() { export function Lobby() {
const navigate = useNavigate(); const navigate = useNavigate();
const mx = useMatrixClient(); const mx = useMatrixClient();
const theme = useTheme();
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]); const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
@@ -432,7 +430,7 @@ export function Lobby() {
return ( return (
<PowerLevelsContextProvider value={spacePowerLevels}> <PowerLevelsContextProvider value={spacePowerLevels}>
<Box grow="Yes"> <Box grow="Yes">
<Page transparent={theme.flat}> <Page>
<LobbyHeader <LobbyHeader
showProfile={!onTop} showProfile={!onTop}
powerLevels={roomsPowerLevels.get(space.roomId) ?? {}} powerLevels={roomsPowerLevels.get(space.roomId) ?? {}}
+3 -12
View File
@@ -165,7 +165,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
<Box shrink="No"> <Box shrink="No">
<BackRouteHandler> <BackRouteHandler>
{(onBack) => ( {(onBack) => (
<IconButton fill="None" onClick={onBack}> <IconButton onClick={onBack}>
<Icon src={Icons.ArrowLeft} /> <Icon src={Icons.ArrowLeft} />
</IconButton> </IconButton>
)} )}
@@ -218,11 +218,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
} }
> >
{(triggerRef) => ( {(triggerRef) => (
<IconButton <IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}>
fill="None"
ref={triggerRef}
onClick={() => setPeopleDrawer((drawer) => !drawer)}
>
<Icon size="400" src={Icons.User} /> <Icon size="400" src={Icons.User} />
</IconButton> </IconButton>
)} )}
@@ -239,12 +235,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
} }
> >
{(triggerRef) => ( {(triggerRef) => (
<IconButton <IconButton onClick={handleOpenMenu} ref={triggerRef} aria-pressed={!!menuAnchor}>
fill="None"
onClick={handleOpenMenu}
ref={triggerRef}
aria-pressed={!!menuAnchor}
>
<Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} /> <Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} />
</IconButton> </IconButton>
)} )}
+3 -5
View File
@@ -175,7 +175,6 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf
type RoomProfileProps = { type RoomProfileProps = {
roomId: string; roomId: string;
roomType?: string;
name: string; name: string;
topic?: string; topic?: string;
avatarUrl?: string; avatarUrl?: string;
@@ -186,7 +185,6 @@ type RoomProfileProps = {
}; };
function RoomProfile({ function RoomProfile({
roomId, roomId,
roomType,
name, name,
topic, topic,
avatarUrl, avatarUrl,
@@ -202,7 +200,9 @@ function RoomProfile({
roomId={roomId} roomId={roomId}
src={avatarUrl} src={avatarUrl}
alt={name} alt={name}
renderFallback={() => <RoomIcon size="300" joinRule={joinRule} roomType={roomType} />} renderFallback={() => (
<RoomIcon size="300" joinRule={joinRule ?? JoinRule.Restricted} filled />
)}
/> />
</Avatar> </Avatar>
<Box grow="Yes" direction="Column"> <Box grow="Yes" direction="Column">
@@ -338,7 +338,6 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
{(localSummary) => ( {(localSummary) => (
<RoomProfile <RoomProfile
roomId={roomId} roomId={roomId}
roomType={localSummary.roomType}
name={localSummary.name} name={localSummary.name}
topic={localSummary.topic} topic={localSummary.topic}
avatarUrl={ avatarUrl={
@@ -397,7 +396,6 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
{summary && ( {summary && (
<RoomProfile <RoomProfile
roomId={roomId} roomId={roomId}
roomType={summary.room_type}
name={summary.name || summary.canonical_alias || roomId} name={summary.name || summary.canonical_alias || roomId}
topic={summary.topic} topic={summary.topic}
avatarUrl={ avatarUrl={
+4 -20
View File
@@ -36,8 +36,6 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal'; import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal';
import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal'; import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal';
import { AddExistingModal } from '../add-existing'; import { AddExistingModal } from '../add-existing';
import { CreateRoomType } from '../../components/create-room/types';
import { BetaNoticeBadge } from '../../components/BetaNoticeBadge';
function SpaceProfileLoading() { function SpaceProfileLoading() {
return ( return (
@@ -64,7 +62,6 @@ function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfil
as="span" as="span"
className={css.HeaderChip} className={css.HeaderChip}
variant="Surface" variant="Surface"
fill="None"
size="500" size="500"
before={ before={
<Avatar size="200" radii="300"> <Avatar size="200" radii="300">
@@ -122,7 +119,6 @@ function UnjoinedSpaceProfile({
<Chip <Chip
className={css.HeaderChip} className={css.HeaderChip}
variant="Surface" variant="Surface"
fill="None"
size="500" size="500"
onClick={join} onClick={join}
disabled={!canJoin} disabled={!canJoin}
@@ -189,7 +185,6 @@ function SpaceProfile({
onClick={handleClose} onClick={handleClose}
className={css.HeaderChip} className={css.HeaderChip}
variant="Surface" variant="Surface"
fill="None"
size="500" size="500"
before={ before={
<Avatar size="200" radii="300"> <Avatar size="200" radii="300">
@@ -233,7 +228,6 @@ function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileP
onClick={handleClose} onClick={handleClose}
className={css.HeaderChip} className={css.HeaderChip}
variant="Surface" variant="Surface"
fill="None"
size="500" size="500"
after={<Icon src={closed ? Icons.ChevronRight : Icons.ChevronBottom} size="50" />} after={<Icon src={closed ? Icons.ChevronRight : Icons.ChevronBottom} size="50" />}
> >
@@ -255,8 +249,8 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
setCords(evt.currentTarget.getBoundingClientRect()); setCords(evt.currentTarget.getBoundingClientRect());
}; };
const handleCreateRoom = (type?: CreateRoomType) => { const handleCreateRoom = () => {
openCreateRoomModal(item.roomId, type); openCreateRoomModal(item.roomId);
setCords(undefined); setCords(undefined);
}; };
@@ -287,19 +281,9 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
radii="300" radii="300"
variant="Primary" variant="Primary"
fill="None" fill="None"
onClick={() => handleCreateRoom(CreateRoomType.TextRoom)} onClick={handleCreateRoom}
> >
<Text size="T300">Chat Room</Text> <Text size="T300">New Room</Text>
</MenuItem>
<MenuItem
size="300"
radii="300"
variant="Primary"
fill="None"
onClick={() => handleCreateRoom(CreateRoomType.VoiceRoom)}
after={<BetaNoticeBadge />}
>
<Text size="T300">Voice Room</Text>
</MenuItem> </MenuItem>
<MenuItem size="300" radii="300" fill="None" onClick={handleAddExisting}> <MenuItem size="300" radii="300" fill="None" onClick={handleAddExisting}>
<Text size="T300">Existing Room</Text> <Text size="T300">Existing Room</Text>
@@ -29,7 +29,7 @@ import { SearchOrderBy } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getRoomIconSrc } from '../../utils/room'; import { joinRuleToIconSrc } from '../../utils/room';
import { factoryRoomIdByAtoZ } from '../../utils/sort'; import { factoryRoomIdByAtoZ } from '../../utils/sort';
import { import {
SearchItemStrGetter, SearchItemStrGetter,
@@ -274,7 +274,9 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
before={ before={
<Icon <Icon
size="50" size="50"
src={getRoomIconSrc(Icons, room.getType(), room.getJoinRule())} src={
joinRuleToIconSrc(Icons, room.getJoinRule(), false) ?? Icons.Hash
}
/> />
} }
> >
@@ -390,7 +392,10 @@ export function SearchFilters({
onClick={() => onSelectedRoomsChange(selectedRooms.filter((rId) => rId !== roomId))} onClick={() => onSelectedRoomsChange(selectedRooms.filter((rId) => rId !== roomId))}
radii="Pill" radii="Pill"
before={ before={
<Icon size="50" src={getRoomIconSrc(Icons, room.getType(), room.getJoinRule())} /> <Icon
size="50"
src={joinRuleToIconSrc(Icons, room.getJoinRule(), false) ?? Icons.Hash}
/>
} }
after={<Icon size="50" src={Icons.Cross} />} after={<Icon size="50" src={Icons.Cross} />}
> >
@@ -203,12 +203,7 @@ export function SearchResultGroup({
src={getRoomAvatarUrl(mx, room, 96, useAuthentication)} src={getRoomAvatarUrl(mx, room, 96, useAuthentication)}
alt={room.name} alt={room.name}
renderFallback={() => ( renderFallback={() => (
<RoomIcon <RoomIcon size="50" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled />
size="50"
roomType={room.getType()}
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
filled
/>
)} )}
/> />
</Avatar> </Avatar>
@@ -9,7 +9,6 @@ export const RoomNavCategoryButton = as<'button', { closed?: boolean }>(
className={classNames(css.CategoryButton, className)} className={classNames(css.CategoryButton, className)}
variant="Background" variant="Background"
radii="Pill" radii="Pill"
fill="None"
before={ before={
<Icon <Icon
className={css.CategoryButtonIcon} className={css.CategoryButtonIcon}
+6 -78
View File
@@ -19,7 +19,6 @@ import {
} from 'folds'; } from 'folds';
import { useFocusWithin, useHover } from 'react-aria'; import { useFocusWithin, useHover } from 'react-aria';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { useAtom, useAtomValue } from 'jotai';
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav'; import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge'; import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
@@ -52,13 +51,6 @@ import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationS
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 { useRoomName } from '../../hooks/useRoomMeta';
import { useCallMembers, useCallSession } from '../../hooks/useCall';
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
import { callChatAtom } from '../../state/callEmbed';
import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
import { livekitSupport } from '../../hooks/useLivekitSupport';
type RoomNavItemMenuProps = { type RoomNavItemMenuProps = {
room: Room; room: Room;
@@ -217,24 +209,6 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
} }
); );
function CallChatToggle() {
const [chat, setChat] = useAtom(callChatAtom);
return (
<IconButton
onClick={() => setChat(!chat)}
aria-pressed={chat}
aria-label="Toggle Chat"
variant="Background"
fill="None"
size="300"
radii="300"
>
<Icon size="50" src={Icons.Message} filled={chat} />
</IconButton>
);
}
type RoomNavItemProps = { type RoomNavItemProps = {
room: Room; room: Room;
selected: boolean; selected: boolean;
@@ -262,8 +236,6 @@ export function RoomNavItem({
(receipt) => receipt.userId !== mx.getUserId() (receipt) => receipt.userId !== mx.getUserId()
); );
const roomName = useRoomName(room);
const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => { const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => {
evt.preventDefault(); evt.preventDefault();
setMenuAnchor({ setMenuAnchor({
@@ -279,29 +251,6 @@ export function RoomNavItem({
}; };
const optionsVisible = hover || !!menuAnchor; const optionsVisible = hover || !!menuAnchor;
const callSession = useCallSession(room);
const callMembers = useCallMembers(room, callSession);
const startCall = useCallStart(direct);
const callEmbed = useCallEmbed();
const callPref = useAtomValue(useCallPreferencesAtom());
const autoDiscoveryInfo = useAutoDiscoveryInfo();
const handleStartCall: MouseEventHandler<HTMLAnchorElement> = (evt) => {
// Do not join if no livekit support or call is not started by others
if (!livekitSupport(autoDiscoveryInfo) && callMembers.length === 0) {
return;
}
// Do not join if already in call
if (callEmbed) {
return;
}
// Start call in second click
if (selected) {
evt.preventDefault();
startCall(room, callPref);
}
};
return ( return (
<NavItem <NavItem
@@ -314,7 +263,7 @@ export function RoomNavItem({
{...hoverProps} {...hoverProps}
{...focusWithinProps} {...focusWithinProps}
> >
<NavLink to={linkPath} onClick={room.isCallRoom() ? handleStartCall : undefined}> <NavLink to={linkPath}>
<NavItemContent> <NavItemContent>
<Box as="span" grow="Yes" alignItems="Center" gap="200"> <Box as="span" grow="Yes" alignItems="Center" gap="200">
<Avatar size="200" radii="400"> <Avatar size="200" radii="400">
@@ -326,28 +275,25 @@ export function RoomNavItem({
? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
: getRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication)
} }
alt={roomName} alt={room.name}
renderFallback={() => ( renderFallback={() => (
<Text as="span" size="H6"> <Text as="span" size="H6">
{nameInitials(roomName)} {nameInitials(room.name)}
</Text> </Text>
)} )}
/> />
) : ( ) : (
<RoomIcon <RoomIcon
style={{ style={{ opacity: unread ? config.opacity.P500 : config.opacity.P300 }}
opacity: unread ? config.opacity.P500 : config.opacity.P300,
}}
filled={selected} filled={selected}
size="100" size="100"
joinRule={room.getJoinRule()} joinRule={room.getJoinRule()}
roomType={room.getType()}
/> />
)} )}
</Avatar> </Avatar>
<Box as="span" grow="Yes"> <Box as="span" grow="Yes">
<Text priority={unread ? '500' : '300'} as="span" size="Inherit" truncate> <Text priority={unread ? '500' : '300'} as="span" size="Inherit" truncate>
{roomName} {room.name}
</Text> </Text>
</Box> </Box>
{!optionsVisible && !unread && !selected && typingMember.length > 0 && ( {!optionsVisible && !unread && !selected && typingMember.length > 0 && (
@@ -361,30 +307,14 @@ export function RoomNavItem({
</UnreadBadgeCenter> </UnreadBadgeCenter>
)} )}
{!optionsVisible && notificationMode !== RoomNotificationMode.Unset && ( {!optionsVisible && notificationMode !== RoomNotificationMode.Unset && (
<Icon <Icon size="50" src={getRoomNotificationModeIcon(notificationMode)} />
size="50"
src={getRoomNotificationModeIcon(notificationMode)}
aria-label={notificationMode}
/>
)}
{room.isCallRoom() && callMembers.length > 0 && (
<Badge variant="Critical" fill="Solid" size="400">
<Text as="span" size="L400" truncate>
{callMembers.length} Live
</Text>
</Badge>
)} )}
</Box> </Box>
</NavItemContent> </NavItemContent>
</NavLink> </NavLink>
{optionsVisible && ( {optionsVisible && (
<NavItemOptions> <NavItemOptions>
{selected && (callEmbed?.roomId === room.roomId || room.isCallRoom()) && (
<CallChatToggle />
)}
<PopOut <PopOut
id={`menu-${room.roomId}`}
aria-expanded={!!menuAnchor}
anchor={menuAnchor} anchor={menuAnchor}
offset={menuAnchor?.width === 0 ? 0 : undefined} offset={menuAnchor?.width === 0 ? 0 : undefined}
alignOffset={menuAnchor?.width === 0 ? 0 : -5} alignOffset={menuAnchor?.width === 0 ? 0 : -5}
@@ -413,8 +343,6 @@ export function RoomNavItem({
<IconButton <IconButton
onClick={handleOpenMenu} onClick={handleOpenMenu}
aria-pressed={!!menuAnchor} aria-pressed={!!menuAnchor}
aria-controls={`menu-${room.roomId}`}
aria-label="More Options"
variant="Background" variant="Background"
fill="None" fill="None"
size="300" size="300"
@@ -104,7 +104,6 @@ export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
renderFallback={() => ( renderFallback={() => (
<RoomIcon <RoomIcon
size="50" size="50"
roomType={room.getType()}
joinRule={joinRuleContent?.join_rule ?? JoinRule.Invite} joinRule={joinRuleContent?.join_rule ?? JoinRule.Invite}
filled filled
/> />
@@ -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">Advanced Options</Text> <Text size="L400">Advance Options</Text>
<RoomUpgrade permissions={permissions} requestClose={requestClose} /> <RoomUpgrade permissions={permissions} requestClose={requestClose} />
</Box> </Box>
</Box> </Box>
@@ -23,7 +23,7 @@ export function Permissions({ requestClose }: PermissionsProps) {
const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId()); const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId());
const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId()); const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId());
const permissionGroups = usePermissionGroups(room.isCallRoom()); const permissionGroups = usePermissionGroups();
const [powerEditor, setPowerEditor] = useState(false); const [powerEditor, setPowerEditor] = useState(false);
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { MessageEvent, StateEvent } from '../../../../types/matrix/room'; import { MessageEvent, StateEvent } from '../../../../types/matrix/room';
import { PermissionGroup } from '../../common-settings/permissions'; import { PermissionGroup } from '../../common-settings/permissions';
export const usePermissionGroups = (isCallRoom: boolean): PermissionGroup[] => { export const usePermissionGroups = (): PermissionGroup[] => {
const groups: PermissionGroup[] = useMemo(() => { const groups: PermissionGroup[] = useMemo(() => {
const messagesGroup: PermissionGroup = { const messagesGroup: PermissionGroup = {
name: 'Messages', name: 'Messages',
@@ -46,19 +46,6 @@ export const usePermissionGroups = (isCallRoom: boolean): PermissionGroup[] => {
], ],
}; };
const callSettingsGroup: PermissionGroup = {
name: 'Calls',
items: [
{
location: {
state: true,
key: StateEvent.GroupCallMemberPrefix,
},
name: 'Join Call',
},
],
};
const moderationGroup: PermissionGroup = { const moderationGroup: PermissionGroup = {
name: 'Moderation', name: 'Moderation',
items: [ items: [
@@ -190,13 +177,6 @@ export const usePermissionGroups = (isCallRoom: boolean): 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,
@@ -216,13 +196,12 @@ export const usePermissionGroups = (isCallRoom: boolean): PermissionGroup[] => {
return [ return [
messagesGroup, messagesGroup,
...(isCallRoom ? [callSettingsGroup] : []),
moderationGroup, moderationGroup,
roomOverviewGroup, roomOverviewGroup,
roomSettingsGroup, roomSettingsGroup,
otherSettingsGroup, otherSettingsGroup,
]; ];
}, [isCallRoom]); }, []);
return groups; return groups;
}; };
-60
View File
@@ -1,60 +0,0 @@
import React from 'react';
import { useSetAtom } from 'jotai';
import { useParams } from 'react-router-dom';
import { Box, Text, TooltipProvider, Tooltip, Icon, Icons, IconButton, toRem } from 'folds';
import { Page, PageHeader } from '../../components/page';
import { callChatAtom } from '../../state/callEmbed';
import { RoomView } from './RoomView';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { useTheme } from '../../hooks/useTheme';
export function CallChatView() {
const { eventId } = useParams();
const theme = useTheme();
const setChat = useSetAtom(callChatAtom);
const screenSize = useScreenSizeContext();
const handleClose = () => setChat(false);
return (
<Page
transparent={theme.flat}
style={{
width: screenSize === ScreenSize.Desktop ? toRem(456) : '100%',
flexShrink: 0,
flexGrow: 0,
}}
>
<PageHeader>
<Box grow="Yes" alignItems="Center" gap="200">
<Box grow="Yes">
<Text size="H5" truncate>
Chat
</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" fill="None" onClick={handleClose}>
<Icon src={Icons.Cross} />
</IconButton>
)}
</TooltipProvider>
</Box>
</Box>
</PageHeader>
<Box grow="Yes" direction="Column">
<RoomView eventId={eventId} />
</Box>
</Page>
);
}
+8 -8
View File
@@ -51,11 +51,12 @@ 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 { useGetMemberPowerLevel, usePowerLevelsContext } from '../../hooks/usePowerLevels'; import { 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';
import { useSpaceOptionally } from '../../hooks/useSpace'; import { useSpaceOptionally } from '../../hooks/useSpace';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag'; import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag';
import { useRoomCreators } from '../../hooks/useRoomCreators'; import { useRoomCreators } from '../../hooks/useRoomCreators';
@@ -88,7 +89,6 @@ function MemberDrawerHeader({ room }: MemberDrawerHeaderProps) {
<IconButton <IconButton
ref={triggerRef} ref={triggerRef}
variant="Background" variant="Background"
fill="None"
onClick={() => setPeopleDrawer(false)} onClick={() => setPeopleDrawer(false)}
> >
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
@@ -132,7 +132,6 @@ function MemberItem({
aria-pressed={pressed} aria-pressed={pressed}
data-user-id={member.userId} data-user-id={member.userId}
variant="Background" variant="Background"
fill="None"
radii="400" radii="400"
onClick={onClick} onClick={onClick}
before={ before={
@@ -186,7 +185,6 @@ 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();
@@ -200,7 +198,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, getPowerLevel); const memberPowerSort = useMemberPowerSort(creators);
const typingMembers = useRoomTypingMember(room.roomId); const typingMembers = useRoomTypingMember(room.roomId);
@@ -246,7 +244,11 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
}; };
return ( return (
<Box className={classNames(css.MembersDrawer)} shrink="No" direction="Column"> <Box
className={classNames(css.MembersDrawer, ContainerColor({ variant: 'Background' }))}
shrink="No"
direction="Column"
>
<MemberDrawerHeader room={room} /> <MemberDrawerHeader room={room} />
<Box className={css.MemberDrawerContentBase} grow="Yes"> <Box className={css.MemberDrawerContentBase} grow="Yes">
<Scroll ref={scrollRef} variant="Background" size="300" visibility="Hover" hideTrack> <Scroll ref={scrollRef} variant="Background" size="300" visibility="Hover" hideTrack>
@@ -276,7 +278,6 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
)) as MouseEventHandler<HTMLButtonElement> )) as MouseEventHandler<HTMLButtonElement>
} }
variant="Background" variant="Background"
fill="None"
size="400" size="400"
radii="300" radii="300"
before={<Icon src={Icons.Filter} size="50" />} before={<Icon src={Icons.Filter} size="50" />}
@@ -309,7 +310,6 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
)) as MouseEventHandler<HTMLButtonElement> )) as MouseEventHandler<HTMLButtonElement>
} }
variant="Background" variant="Background"
fill="None"
size="400" size="400"
radii="300" radii="300"
after={<Icon src={Icons.Sort} size="50" />} after={<Icon src={Icons.Sort} size="50" />}
+16 -39
View File
@@ -2,7 +2,6 @@ import React, { useCallback } from 'react';
import { Box, Line } from 'folds'; import { Box, Line } from 'folds';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { isKeyHotkey } from 'is-hotkey'; import { isKeyHotkey } from 'is-hotkey';
import { useAtomValue } from 'jotai';
import { RoomView } from './RoomView'; import { RoomView } from './RoomView';
import { MembersDrawer } from './MembersDrawer'; import { MembersDrawer } from './MembersDrawer';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
@@ -14,25 +13,21 @@ 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 { CallView } from '../call/CallView'; import { ThreadView } from './thread-view';
import { RoomViewHeader } from './RoomViewHeader'; import { useActiveThread } from '../../state/hooks/roomToActiveThread';
import { callChatAtom } from '../../state/callEmbed';
import { CallChatView } from './CallChatView';
import { Page } from '../../components/page';
import { useTheme } from '../../hooks/useTheme';
export function Room() { export function Room() {
const { eventId } = useParams(); const { eventId } = useParams();
const room = useRoom(); const room = useRoom();
const mx = useMatrixClient(); const mx = useMatrixClient();
const theme = useTheme();
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer'); const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const members = useRoomMembers(mx, room.roomId); const members = useRoomMembers(mx, room.roomId);
const chat = useAtomValue(callChatAtom);
const threadId = useActiveThread(room.roomId);
useKeyDown( useKeyDown(
window, window,
@@ -46,41 +41,23 @@ export function Room() {
) )
); );
const callView = room.isCallRoom();
return ( return (
<PowerLevelsContextProvider value={powerLevels}> <PowerLevelsContextProvider value={powerLevels}>
<Box grow="Yes"> <Box grow="Yes">
{callView && (screenSize === ScreenSize.Desktop || !chat) && ( <RoomView room={room} eventId={eventId} />
<Page transparent={theme.flat}> {threadId ? (
<RoomViewHeader callView />
<Box grow="Yes">
<CallView />
</Box>
</Page>
)}
{!callView && (
<Page transparent={theme.flat}>
<RoomViewHeader />
<Box grow="Yes">
<RoomView eventId={eventId} />
</Box>
</Page>
)}
{callView && chat && (
<> <>
{screenSize === ScreenSize.Desktop && ( <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} />
<CallChatView /> </>
</> )
)}
{!callView && screenSize === ScreenSize.Desktop && isDrawer && (
<>
<Line variant="Background" direction="Vertical" size="300" />
<MembersDrawer key={room.roomId} room={room} members={members} />
</>
)} )}
</Box> </Box>
</PowerLevelsContextProvider> </PowerLevelsContextProvider>
+1 -1
View File
@@ -221,7 +221,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const isComposing = useComposingCheck(); const isComposing = useComposingCheck();
useElementSizeObserver( useElementSizeObserver(
useCallback(() => fileDropContainerRef.current, [fileDropContainerRef]), useCallback(() => document.body, []),
useCallback((width) => setHideStickerBtn(width < 500), []) useCallback((width) => setHideStickerBtn(width < 500), [])
); );
+41 -129
View File
@@ -27,7 +27,6 @@ import { HTMLReactParserOptions } from 'html-react-parser';
import classNames from 'classnames'; import classNames from 'classnames';
import { ReactEditor } from 'slate-react'; import { ReactEditor } from 'slate-react';
import { Editor } from 'slate'; import { Editor } from 'slate';
import { SessionMembershipData } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
import to from 'await-to-js'; import to from 'await-to-js';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import { import {
@@ -78,6 +77,7 @@ import {
decryptAllTimelineEvent, decryptAllTimelineEvent,
getEditedEvent, getEditedEvent,
getEventReactions, getEventReactions,
getEventThreadDetail,
getLatestEditableEvt, getLatestEditableEvt,
getMemberDisplayName, getMemberDisplayName,
getReactionContent, getReactionContent,
@@ -127,6 +127,18 @@ 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) => (
@@ -151,79 +163,6 @@ 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;
@@ -233,6 +172,14 @@ 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;
@@ -472,7 +419,6 @@ 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>();
@@ -484,6 +430,7 @@ 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);
@@ -1016,6 +963,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
}, },
[editor] [editor]
); );
const { t } = useTranslation(); const { t } = useTranslation();
const renderMatrixEvent = useMatrixEventRenderer< const renderMatrixEvent = useMatrixEventRenderer<
@@ -1036,6 +984,7 @@ 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
@@ -1049,7 +998,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 || (canDeleteOwn && mEvent.getSender() === mx.getUserId())} canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
canSendReaction={canSendReaction} canSendReaction={canSendReaction}
canPinEvent={canPinEvent} canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms} imagePackRooms={imagePackRooms}
@@ -1109,6 +1058,20 @@ 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>
); );
}, },
@@ -1131,7 +1094,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 || (canDeleteOwn && mEvent.getSender() === mx.getUserId())} canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
canSendReaction={canSendReaction} canSendReaction={canSendReaction}
canPinEvent={canPinEvent} canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms} imagePackRooms={imagePackRooms}
@@ -1249,7 +1212,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
messageLayout={messageLayout} messageLayout={messageLayout}
collapse={collapse} collapse={collapse}
highlight={highlighted} highlight={highlighted}
canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())} canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
canSendReaction={canSendReaction} canSendReaction={canSendReaction}
canPinEvent={canPinEvent} canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms} imagePackRooms={imagePackRooms}
@@ -1470,57 +1433,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
</Event> </Event>
); );
}, },
[StateEvent.GroupCallMemberPrefix]: (mEventId, mEvent, item) => {
const highlighted = focusItem?.index === item && focusItem.highlight;
const senderId = mEvent.getSender() ?? '';
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const content = mEvent.getContent<SessionMembershipData>();
const prevContent = mEvent.getPrevContent();
const callJoined = content.application;
if (callJoined && 'application' in prevContent) {
return null;
}
const timeJSX = (
<Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
);
return (
<Event
key={mEvent.getId()}
data-message-item={item}
data-message-id={mEventId}
room={room}
mEvent={mEvent}
highlight={highlighted}
messageSpacing={messageSpacing}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
hideReadReceipts={hideActivity}
showDeveloperTools={showDeveloperTools}
>
<EventContent
messageLayout={messageLayout}
time={timeJSX}
iconSrc={callJoined ? Icons.Phone : Icons.PhoneDown}
content={
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<b>{senderName}</b>
{callJoined ? ' joined the call' : ' ended the call'}
</Text>
</Box>
}
/>
</Event>
);
},
}, },
(mEventId, mEvent, item) => { (mEventId, mEvent, item) => {
if (!showHiddenEvents) return null; if (!showHiddenEvents) return null;
+9 -6
View File
@@ -1,6 +1,6 @@
import React, { useCallback, useRef } from 'react'; import React, { useCallback, useRef } from 'react';
import { Box, Text, config } from 'folds'; import { Box, Text, config } from 'folds';
import { EventType } from 'matrix-js-sdk'; import { EventType, Room } from 'matrix-js-sdk';
import { ReactEditor } from 'slate-react'; import { ReactEditor } from 'slate-react';
import { isKeyHotkey } from 'is-hotkey'; import { isKeyHotkey } from 'is-hotkey';
import { useStateEvent } from '../../hooks/useStateEvent'; import { useStateEvent } from '../../hooks/useStateEvent';
@@ -14,13 +14,14 @@ import { RoomViewTyping } from './RoomViewTyping';
import { RoomTombstone } from './RoomTombstone'; import { RoomTombstone } from './RoomTombstone';
import { RoomInput } from './RoomInput'; import { RoomInput } from './RoomInput';
import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing'; import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing';
import { Page } from '../../components/page';
import { RoomViewHeader } from './RoomViewHeader';
import { useKeyDown } from '../../hooks/useKeyDown'; import { useKeyDown } from '../../hooks/useKeyDown';
import { editableActiveElement } from '../../utils/dom'; import { editableActiveElement } from '../../utils/dom';
import { settingsAtom } from '../../state/settings'; import { settingsAtom } from '../../state/settings';
import { useSetting } from '../../state/hooks/settings'; import { useSetting } from '../../state/hooks/settings';
import { useRoomPermissions } from '../../hooks/useRoomPermissions'; import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { useRoomCreators } from '../../hooks/useRoomCreators'; import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoom } from '../../hooks/useRoom';
const FN_KEYS_REGEX = /^F\d+$/; const FN_KEYS_REGEX = /^F\d+$/;
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => { const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
@@ -29,8 +30,10 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
return false; return false;
} }
// do not focus on F keys
if (FN_KEYS_REGEX.test(code)) return false; if (FN_KEYS_REGEX.test(code)) return false;
// do not focus on numlock/scroll lock
if ( if (
code.startsWith('OS') || code.startsWith('OS') ||
code.startsWith('Meta') || code.startsWith('Meta') ||
@@ -53,13 +56,12 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
return true; return true;
}; };
export function RoomView({ eventId }: { eventId?: string }) { export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
const roomInputRef = useRef<HTMLDivElement>(null); const roomInputRef = useRef<HTMLDivElement>(null);
const roomViewRef = useRef<HTMLDivElement>(null); const roomViewRef = useRef<HTMLDivElement>(null);
const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const room = useRoom();
const { roomId } = room; const { roomId } = room;
const editor = useEditor(); const editor = useEditor();
@@ -90,7 +92,8 @@ export function RoomView({ eventId }: { eventId?: string }) {
); );
return ( return (
<Box grow="Yes" direction="Column" ref={roomViewRef}> <Page ref={roomViewRef}>
<RoomViewHeader />
<Box grow="Yes" direction="Column"> <Box grow="Yes" direction="Column">
<RoomTimeline <RoomTimeline
key={roomId} key={roomId}
@@ -134,6 +137,6 @@ export function RoomView({ eventId }: { eventId?: string }) {
</div> </div>
{hideActivity ? <RoomViewFollowingPlaceholder /> : <RoomViewFollowing room={room} />} {hideActivity ? <RoomViewFollowingPlaceholder /> : <RoomViewFollowing room={room} />}
</Box> </Box>
</Box> </Page>
); );
} }
@@ -16,6 +16,8 @@ export const RoomViewFollowing = recipe({
minHeight: toRem(28), minHeight: toRem(28),
padding: `0 ${config.space.S400}`, padding: `0 ${config.space.S400}`,
width: '100%', width: '100%',
backgroundColor: color.Surface.Container,
color: color.Surface.OnContainer,
outline: 'none', outline: 'none',
}, },
], ],
+64 -35
View File
@@ -23,7 +23,9 @@ import {
Spinner, Spinner,
} from 'folds'; } from 'folds';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Room } from 'matrix-js-sdk'; import { JoinRule, Room } from 'matrix-js-sdk';
import { useAtomValue } from 'jotai';
import { useStateEvent } from '../../hooks/useStateEvent'; import { useStateEvent } from '../../hooks/useStateEvent';
import { PageHeader } from '../../components/page'; import { PageHeader } from '../../components/page';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
@@ -31,7 +33,7 @@ import { UseStateProvider } from '../../components/UseStateProvider';
import { RoomTopicViewer } from '../../components/room-topic-viewer'; import { RoomTopicViewer } from '../../components/room-topic-viewer';
import { StateEvent } from '../../../types/matrix/room'; import { StateEvent } from '../../../types/matrix/room';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom'; import { useRoom } from '../../hooks/useRoom';
import { useSetting } from '../../state/hooks/settings'; import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings'; import { settingsAtom } from '../../state/settings';
import { useSpaceOptionally } from '../../hooks/useSpace'; import { useSpaceOptionally } from '../../hooks/useSpace';
@@ -46,6 +48,7 @@ import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { copyToClipboard } from '../../utils/dom'; import { copyToClipboard } from '../../utils/dom';
import { LeaveRoomPrompt } from '../../components/leave-room-prompt'; import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta'; import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { getMatrixToRoom } from '../../plugins/matrix-to'; import { getMatrixToRoom } from '../../plugins/matrix-to';
@@ -66,7 +69,7 @@ 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 { RoomSettingsPage } from '../../state/roomSettings'; import { ThreadsMenu } from './threads-menu';
type RoomMenuProps = { type RoomMenuProps = {
room: Room; room: Room;
@@ -252,7 +255,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
); );
}); });
export function RoomViewHeader({ callView }: { callView?: boolean }) { export function RoomViewHeader() {
const navigate = useNavigate(); const navigate = useNavigate();
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
@@ -261,12 +264,13 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
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 direct = useIsDirectRoom(); const [threadsMenuAnchor, setThreadsMenuAnchor] = useState<RectCords>();
const mDirects = useAtomValue(mDirectAtom);
const pinnedEvents = useRoomPinnedEvents(room); const pinnedEvents = useRoomPinnedEvents(room);
const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption); const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption);
const encryptedRoom = !!encryptionEvent; const ecryptedRoom = !!encryptionEvent;
const avatarMxc = useRoomAvatar(room, direct); const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
const name = useRoomName(room); const name = useRoomName(room);
const topic = useRoomTopic(room); const topic = useRoomTopic(room);
const avatarUrl = avatarMxc const avatarUrl = avatarMxc
@@ -293,14 +297,8 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
setPinMenuAnchor(evt.currentTarget.getBoundingClientRect()); setPinMenuAnchor(evt.currentTarget.getBoundingClientRect());
}; };
const openSettings = useOpenRoomSettings(); const handleOpenThreadsMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
const parentSpace = useSpaceOptionally(); setThreadsMenuAnchor(evt.currentTarget.getBoundingClientRect());
const handleMemberToggle = () => {
if (callView) {
openSettings(room.roomId, parentSpace?.roomId, RoomSettingsPage.MembersPage);
return;
}
setPeopleDrawer(!peopleDrawer);
}; };
return ( return (
@@ -310,7 +308,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
<BackRouteHandler> <BackRouteHandler>
{(onBack) => ( {(onBack) => (
<Box shrink="No" alignItems="Center"> <Box shrink="No" alignItems="Center">
<IconButton fill="None" onClick={onBack}> <IconButton onClick={onBack}>
<Icon src={Icons.ArrowLeft} /> <Icon src={Icons.ArrowLeft} />
</IconButton> </IconButton>
</Box> </Box>
@@ -325,7 +323,11 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
src={avatarUrl} src={avatarUrl}
alt={name} alt={name}
renderFallback={() => ( renderFallback={() => (
<RoomIcon size="200" joinRule={room.getJoinRule()} roomType={room.getType()} /> <RoomIcon
size="200"
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
filled
/>
)} )}
/> />
</Avatar> </Avatar>
@@ -373,9 +375,8 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
)} )}
</Box> </Box>
</Box> </Box>
<Box shrink="No"> <Box shrink="No">
{!encryptedRoom && ( {!ecryptedRoom && (
<TooltipProvider <TooltipProvider
position="Bottom" position="Bottom"
offset={4} offset={4}
@@ -386,7 +387,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
} }
> >
{(triggerRef) => ( {(triggerRef) => (
<IconButton fill="None" ref={triggerRef} onClick={handleSearchClick}> <IconButton ref={triggerRef} onClick={handleSearchClick}>
<Icon size="400" src={Icons.Search} /> <Icon size="400" src={Icons.Search} />
</IconButton> </IconButton>
)} )}
@@ -403,7 +404,6 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
> >
{(triggerRef) => ( {(triggerRef) => (
<IconButton <IconButton
fill="None"
style={{ position: 'relative' }} style={{ position: 'relative' }}
onClick={handleOpenPinMenu} onClick={handleOpenPinMenu}
ref={triggerRef} ref={triggerRef}
@@ -430,6 +430,27 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
</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"
@@ -449,29 +470,42 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
</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"
offset={4} offset={4}
tooltip={ tooltip={
<Tooltip> <Tooltip>
{callView ? ( <Text>{peopleDrawer ? 'Hide Members' : 'Show Members'}</Text>
<Text>Members</Text>
) : (
<Text>{peopleDrawer ? 'Hide Members' : 'Show Members'}</Text>
)}
</Tooltip> </Tooltip>
} }
> >
{(triggerRef) => ( {(triggerRef) => (
<IconButton fill="None" ref={triggerRef} onClick={handleMemberToggle}> <IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}>
<Icon size="400" src={Icons.User} /> <Icon size="400" src={Icons.User} />
</IconButton> </IconButton>
)} )}
</TooltipProvider> </TooltipProvider>
)} )}
<TooltipProvider <TooltipProvider
position="Bottom" position="Bottom"
align="End" align="End"
@@ -483,12 +517,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
} }
> >
{(triggerRef) => ( {(triggerRef) => (
<IconButton <IconButton onClick={handleOpenMenu} ref={triggerRef} aria-pressed={!!menuAnchor}>
fill="None"
onClick={handleOpenMenu}
ref={triggerRef}
aria-pressed={!!menuAnchor}
>
<Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} /> <Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} />
</IconButton> </IconButton>
)} )}
+3 -1
View File
@@ -327,9 +327,11 @@ export const MessageCopyLinkItem = as<
const mx = useMatrixClient(); const mx = useMatrixClient();
const handleCopy = () => { const handleCopy = () => {
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
const eventId = mEvent.getId(); const eventId = mEvent.getId();
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
if (!eventId) return; if (!eventId) return;
copyToClipboard(getMatrixToRoomEvent(room.roomId, eventId, getViaServers(room))); copyToClipboard(getMatrixToRoomEvent(roomIdOrAlias, eventId, viaServers));
onClose?.(); onClose?.();
}; };
@@ -0,0 +1,83 @@
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>
);
}
@@ -0,0 +1 @@
export * from './ThreadSelector';
@@ -0,0 +1,37 @@
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,
});
@@ -0,0 +1,79 @@
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>
);
}

Some files were not shown because too many files have changed in this diff Show More