Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 444b2feb9e | |||
| f35aa384e5 | |||
| b5fd41f862 | |||
| 3d4c91c969 | |||
| 38cc6e6f3a | |||
| 12bcbc2e78 | |||
| e44ca92422 | |||
| 174b315278 | |||
| d73428ee3d | |||
| f2c5a595b9 | |||
| a6a3ac3b24 | |||
| 67c6785bf3 | |||
| d36938e1fd | |||
| 1914606895 | |||
| 19096c3543 | |||
| 737cc09fea | |||
| 154f234d0c |
@@ -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": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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. ⚠️
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 }}'
|
||||||
|
|||||||
@@ -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 }}
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
24.13.1
|
|
||||||
+2
-2
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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": {
|
||||||
|
|||||||
Generated
+30
-44
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>;
|
||||||
|
|||||||
+26
-22
@@ -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,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';
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
export enum CreateRoomType {
|
|
||||||
TextRoom = 'text',
|
|
||||||
VoiceRoom = 'voice',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CreateRoomAccess {
|
|
||||||
Private = 'private',
|
|
||||||
Restricted = 'restricted',
|
|
||||||
Public = 'public',
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const Time = style({
|
||||||
|
fontWeight: 'inherit',
|
||||||
|
flexShrink: 0,
|
||||||
|
});
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>;
|
|
||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 +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}`,
|
|
||||||
});
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 chat’s empty — Be the first to hop in!
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NoPermissionMessage() {
|
|
||||||
return (
|
|
||||||
<Text style={{ margin: 'auto' }} size="L400" align="Center">
|
|
||||||
You don'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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) ?? {}}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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" />}
|
||||||
|
|||||||
@@ -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" />
|
||||||
<Line variant="Background" direction="Vertical" size="300" />
|
<ThreadView threadId={threadId} />
|
||||||
)}
|
|
||||||
<CallChatView />
|
|
||||||
</>
|
</>
|
||||||
)}
|
) : (
|
||||||
{!callView && screenSize === ScreenSize.Desktop && isDrawer && (
|
screenSize === ScreenSize.Desktop &&
|
||||||
|
isDrawer && (
|
||||||
<>
|
<>
|
||||||
<Line variant="Background" direction="Vertical" size="300" />
|
<Line variant="Background" direction="Vertical" size="300" />
|
||||||
<MembersDrawer key={room.roomId} room={room} members={members} />
|
<MembersDrawer key={room.roomId} room={room} members={members} />
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</PowerLevelsContextProvider>
|
</PowerLevelsContextProvider>
|
||||||
|
|||||||
@@ -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), [])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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>Members</Text>
|
|
||||||
) : (
|
|
||||||
<Text>{peopleDrawer ? 'Hide Members' : 'Show 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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user