diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml
deleted file mode 100644
index e1a5a06a..00000000
--- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml
+++ /dev/null
@@ -1,127 +0,0 @@
-labels: ["needs-confirmation"]
-body:
- - type: markdown #add faqs in future
- attributes:
- value: |
- > [!IMPORTANT]
- > Please read through [the Discussion rules](https://github.com/cinnyapp/cinny/discussions/2653) and check for both existing [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
- - type: markdown
- attributes:
- value: "# Issue Details"
- - type: textarea
- attributes:
- label: Issue Description
- description: |
- Provide a detailed description of the issue. Include relevant information, such as:
- - The feature or configuration option you encounter the issue with.
- - Screenshots, screen recordings, or other supporting media (as needed).
- - If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description.
- placeholder: |
- When I try to send a message in a room, the message doesn't appear in the timeline.
- OR
- The application crashes when I click on the settings button.
- validations:
- required: true
- - type: textarea
- attributes:
- label: Expected Behavior
- description: |
- Describe how you expect Cinny to behave in this situation.
- placeholder: |
- I expected the message to appear in the room timeline immediately after sending.
- OR
- The settings panel should open smoothly without any crashes.
- validations:
- required: true
- - type: textarea
- attributes:
- label: Actual Behavior
- description: |
- Describe how Cinny actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically.
- placeholder: |
- The application freezes for 3 seconds and then shows a white screen.
- validations:
- required: true
- - type: textarea
- attributes:
- label: Reproduction Steps
- description: |
- Provide a detailed set of step-by-step instructions for reproducing this issue.
- placeholder: |
- 1. Open Cinny and log in to my account
- 2. Navigate to the #general room
- 3. Type a message in the message box
- 4. Press Enter to send
- 5. Notice that the message doesn't appear in the timeline
- validations:
- required: true
- - type: textarea
- attributes:
- label: Environment
- description: |
- Please provide information about your environment. Include the following:
- - OS:
- - Browser:
- - Cinny Web Version: (app.cinny.in or self hosted)
- - Cinny desktop Version: (appimage or deb or flatpak)
- - Matrix Homeserver:
- placeholder: |
- - OS: Windows 11
- - Browser: Chrome 120.0.6099.109
- - Cinny Web Version: 3.2.0 (app.cinny.in or self hosted)
- - Cinny desktop Version: 3.2.0 (appimage or deb or flatpak)
- - Matrix Homeserver: matrix.org (Synapse 1.97.0)
- render: text
- validations:
- required: true
- - type: textarea
- id: logs
- attributes:
- label: Relevant Logs
- description: |
- If applicable, add browser console logs to help explain your problem.
-
- **To get browser console logs:**
- - Chrome/Edge: Press F12 β Console tab
- - Firefox: Press F12 β Console tab
- - Safari: Develop β Show Web Inspector β Console
-
- Please wrap large log outputs in code blocks with triple backticks (```).
- placeholder: |
- ```
- Error: Failed to send message
- at MessageComposer.sendMessage (composer.js:245)
- at HTMLButtonElement.onClick (composer.js:189)
- TypeError: Cannot read property 'content' of undefined
- at RoomTimeline.render (timeline.js:567)
- ```
- render: shell
- validations:
- required: false
- - type: textarea
- attributes:
- label: Additional context
- description: |
- Add any other context about the problem here (e.g., when did this start happening, does it happen on different homeservers, etc.)
- placeholder: |
- - This started happening after I updated to version 3.2.0
- - It only happens in encrypted rooms, not in public rooms
- - I've tried on both Firefox and Chrome with the same result
- - It works fine on my phone using the same account
- - This happens on all homeservers I've tested (matrix.org, mozilla.org)
- validations:
- required: false
- - type: markdown
- attributes:
- value: |
- # User Acknowledgements
- > [!TIP]
- > Use these links to review the existing Cinny [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc).
- - type: checkboxes #add faqs in future
- attributes:
- label: "I acknowledge that:"
- options:
- - label: I have searched the Cinny repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
- required: true
- - label: I have checked the "Preview" tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (` ``` `) on separate lines.
- required: true
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 00000000..76fc578a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,57 @@
+name: π Bug Report
+description: Report a bug
+
+body:
+ - type: markdown
+ attributes:
+ value: |
+ ## First of all
+ 1. Please search for [existing issues](https://github.com/ajbura/cinny/issues?q=is%3Aissue) about this problem first.
+ 2. Make sure Cinny is up to date.
+ 3. Make sure it's an issue with Cinny and not something else you are using.
+ 4. Remember to be friendly.
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Describe the bug
+ description: A clear description of what the bug is. Include screenshots if applicable.
+ placeholder: Bug description
+ validations:
+ required: true
+
+ - type: textarea
+ id: reproduction
+ attributes:
+ label: Reproduction
+ description: Steps to reproduce the behavior.
+ placeholder: |
+ 1. Go to ...
+ 2. Click on ...
+ 3. See error
+
+ - type: textarea
+ id: expected-behavior
+ attributes:
+ label: Expected behavior
+ description: A clear description of what you expected to happen.
+
+ - type: textarea
+ id: info
+ attributes:
+ label: Platform and versions
+ description: "Provide OS, browser and Cinny version with your Homeserver."
+ placeholder: |
+ 1. OS: [e.g. Windows 10, MacOS]
+ 2. Browser: [e.g. chrome 99.5, firefox 97.2]
+ 3. Cinny version: [e.g. 1.8.1 (app.cinny.in)]
+ 4. Matrix homeserver: [e.g. matrix.org]
+ render: shell
+ validations:
+ required: true
+
+ - type: textarea
+ id: context
+ attributes:
+ label: Additional context
+ description: Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index a5eea79c..91ba4f0a 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,5 +1,4 @@
-blank_issues_enabled: false
contact_links:
- - name: Features, Bug Reports, Questions
- url: https://github.com/cinnyapp/cinny/discussions/new/choose
- about: Our preferred starting point if you have any questions or suggestions about features or behavior.
+ - name: π¬ Matrix Chat
+ url: https://matrix.to/#/#cinny:matrix.org
+ about: Ask questions and talk to other Cinny users and the maintainers
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 00000000..412d5181
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,33 @@
+name: π‘ Feature Request
+description: Suggest an idea
+
+body:
+ - type: textarea
+ id: problem
+ attributes:
+ label: Describe the problem
+ description: A clear description of the problem this feature would solve
+ placeholder: "I'm always frustrated when..."
+ validations:
+ required: true
+
+ - type: textarea
+ id: solution
+ attributes:
+ label: "Describe the solution you'd like"
+ description: A clear description of what change you would like
+ placeholder: "I would like to..."
+ validations:
+ required: true
+
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Alternatives considered
+ description: "Any alternative solutions you've considered"
+
+ - type: textarea
+ id: context
+ attributes:
+ label: Additional context
+ description: Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/preapproved.md b/.github/ISSUE_TEMPLATE/preapproved.md
deleted file mode 100644
index 485e727e..00000000
--- a/.github/ISSUE_TEMPLATE/preapproved.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: Pre-Discussed and Approved Topics
-about: |-
- Only for topics already discussed and approved in the GitHub Discussions section.
----
-
-**DO NOT OPEN A NEW ISSUE. PLEASE USE THE DISCUSSIONS SECTION.**
-
-**I DIDN'T READ THE ABOVE LINE. PLEASE CLOSE THIS ISSUE.**
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 4b351db6..1292f1d2 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,9 +1,22 @@
+
+
### Description
-
+
Fixes #
+#### Type of change
+
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] This change requires a documentation update
+
### Checklist:
-- [ ] I have read and understood the [Contribution policy](https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md).
+- [ ] My code follows the style guidelines of this project
+- [ ] I have performed a self-review of my own code
+- [ ] I have commented my code, particularly in hard-to-understand areas
+- [ ] I have made corresponding changes to the documentation
+- [ ] My changes generate no new warnings
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 00000000..2dd642df
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,3 @@
+# Reporting a Vulnerability
+
+**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**
\ No newline at end of file
diff --git a/.github/renovate.json b/.github/renovate.json
index cddac35d..62b0cf2a 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -1,22 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
- "extends": [
- "config:recommended",
- ":dependencyDashboardApproval",
- ":semanticCommits",
- "group:monorepos"
- ],
+ "extends": ["config:recommended", ":dependencyDashboardApproval"],
"labels": ["Dependencies"],
- "rebaseWhen": "conflicted",
"packageRules": [
{
"matchUpdateTypes": ["lockFileMaintenance"]
- },
- {
- "matchPackageNames": ["slate", "slate-dom", "slate-history", "slate-react"]
- },
- {
- "matchPackageNames": ["linkifyjs", "linkify-react"]
}
],
"lockFileMaintenance": {
diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml
index 11b5056c..450e4e29 100644
--- a/.github/workflows/build-pull-request.yml
+++ b/.github/workflows/build-pull-request.yml
@@ -12,12 +12,12 @@ jobs:
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout repository
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ uses: actions/checkout@v4.2.0
- name: Setup node
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
+ uses: actions/setup-node@v4.4.0
with:
- node-version-file: ".node-version"
- package-manager-cache: false
+ node-version: 20.12.2
+ cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build app
@@ -25,7 +25,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build
- name: Upload artifact
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ uses: actions/upload-artifact@v4.6.2
with:
name: preview
path: dist
@@ -33,7 +33,7 @@ jobs:
- name: Save pr number
run: echo ${PR_NUMBER} > ./pr.txt
- name: Upload pr number
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ uses: actions/upload-artifact@v4.6.2
with:
name: pr
path: ./pr.txt
diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml
index 42eab7ef..29fe7eb2 100644
--- a/.github/workflows/cla.yml
+++ b/.github/workflows/cla.yml
@@ -12,7 +12,7 @@ jobs:
- name: 'CLA Assistant'
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
- uses: cla-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
+ uses: cla-assistant/github-action@v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret
diff --git a/.github/workflows/deploy-pull-request.yml b/.github/workflows/deploy-pull-request.yml
index e59681ce..b330c3c1 100644
--- a/.github/workflows/deploy-pull-request.yml
+++ b/.github/workflows/deploy-pull-request.yml
@@ -1,5 +1,4 @@
name: Deploy PR to Netlify
-run-name: "Deploy PR to Netlify (${{ github.event.workflow_run.head_branch }})"
on:
workflow_run:
@@ -16,22 +15,16 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download pr number
- uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
+ uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
with:
workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }}
name: pr
- - name: Validate and output pr number
+ - name: Output pr number
id: pr
- run: |
- PR_ID=$(> "${GITHUB_OUTPUT}"
+ run: echo "id=$(> $GITHUB_OUTPUT
- name: Download artifact
- uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
+ uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
with:
workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }}
@@ -39,7 +32,7 @@ jobs:
path: dist
- name: Deploy to Netlify
id: netlify
- uses: nwtgck/actions-netlify@d22a32a27c918fe470bbc562e984f80ec48c2668 # v4.0.0
+ uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
with:
publish-dir: dist
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
@@ -48,13 +41,16 @@ jobs:
enable-pull-request-comment: false
enable-commit-comment: false
env:
- NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN_PR }}
+ NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PR_CINNY }}
timeout-minutes: 1
- name: Comment preview on PR
- uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
+ uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
- number: ${{ steps.pr.outputs.id }}
+ pr_number: ${{ steps.pr.outputs.id }}
+ comment_tag: ${{ steps.pr.outputs.id }}
message: |
- Preview: ${{ steps.netlify.outputs.deploy-url }}
- β οΈ Exercise caution. Use test accounts. β οΈ
+ Preview: ${{ steps.netlify.outputs.deploy-url }}
+ β οΈ Exercise caution. Use test accounts. β οΈ
\ No newline at end of file
diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml
index 302c3788..398785ab 100644
--- a/.github/workflows/docker-pr.yml
+++ b/.github/workflows/docker-pr.yml
@@ -5,59 +5,15 @@ on:
paths:
- 'Dockerfile'
- '.github/workflows/docker-pr.yml'
- - '.github/workflows/prod-deploy.yml'
jobs:
docker-build:
runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
-
steps:
- name: Checkout repository
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
-
- - name: Set up QEMU
- uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.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@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
- with:
- images: |
- ajbura/cinny
- ghcr.io/${{ github.repository }}
-
- - name: Build Docker image (no push)
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
+ uses: actions/checkout@v4.2.0
+ - name: Build Docker image
+ uses: docker/build-push-action@v6.18.0
with:
context: .
- platforms: linux/amd64
push: false
- load: true
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
-
- - name: Show Docker images
- run: docker images
diff --git a/.github/workflows/lockfile.yml b/.github/workflows/lockfile.yml
index a9cc1a34..be52eb50 100644
--- a/.github/workflows/lockfile.yml
+++ b/.github/workflows/lockfile.yml
@@ -14,9 +14,9 @@ jobs:
pull-requests: write
steps:
- name: Checkout
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ uses: actions/checkout@v4.2.0
- name: NPM Lockfile Changes
- uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891 # v1.0.0
+ uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Optional inputs, can be deleted safely if you are happy with default values.
diff --git a/.github/workflows/netlify-dev.yml b/.github/workflows/netlify-dev.yml
index 12107685..66cd5ad5 100644
--- a/.github/workflows/netlify-dev.yml
+++ b/.github/workflows/netlify-dev.yml
@@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ uses: actions/checkout@v4.2.0
- name: Setup node
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
+ uses: actions/setup-node@v4.4.0
with:
- node-version-file: ".node-version"
- package-manager-cache: false
+ node-version: 20.12.2
+ cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build app
@@ -24,7 +24,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build
- name: Deploy to Netlify
- uses: nwtgck/actions-netlify@d22a32a27c918fe470bbc562e984f80ec48c2668 # v4.0.0
+ uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
with:
publish-dir: dist
deploy-message: 'Dev deploy ${{ github.sha }}'
diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml
deleted file mode 100644
index a52ee8e6..00000000
--- a/.github/workflows/pr-title.yml
+++ /dev/null
@@ -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 }}
diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml
index 2a8192db..7b58f56f 100644
--- a/.github/workflows/prod-deploy.yml
+++ b/.github/workflows/prod-deploy.yml
@@ -10,12 +10,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ uses: actions/checkout@v4.2.0
- name: Setup node
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
+ uses: actions/setup-node@v4.4.0
with:
- node-version-file: '.node-version'
- package-manager-cache: false
+ node-version: 20.12.2
+ cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build app
@@ -23,7 +23,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build
- name: Deploy to Netlify
- uses: nwtgck/actions-netlify@d22a32a27c918fe470bbc562e984f80ec48c2668 # v4.0.0
+ uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
with:
publish-dir: dist
deploy-message: 'Prod deploy ${{ github.ref_name }}'
@@ -52,46 +52,45 @@ jobs:
gpg --export | xxd -p
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
- name: Upload tagged release
- uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
+ uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
with:
- tag_name: ${{ github.ref_name }}
files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
publish-image:
- name: Push Docker image to Docker Hub, GHCR
+ name: Push Docker image to Docker Hub, ghcr
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ uses: actions/checkout@v4.2.0
- name: Set up QEMU
- uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
+ uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- - name: Login to Docker Hub #Do not update this action from a outside PR
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
+ uses: docker/setup-buildx-action@v3.11.1
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3.5.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- - name: Login to the Github Container registry #Do not update this action from a outside PR
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
+ - name: Login to the Container registry
+ uses: docker/login-action@v3.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Extract metadata (tags, labels) for Docker, GHCR
+ - name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
+ uses: docker/metadata-action@v5.8.0
with:
images: |
${{ secrets.DOCKER_USERNAME }}/cinny
ghcr.io/${{ github.repository }}
- name: Build and push Docker image
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
+ uses: docker/build-push-action@v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64
diff --git a/.husky/pre-commit b/.husky/pre-commit
deleted file mode 100644
index 7aea7a8d..00000000
--- a/.husky/pre-commit
+++ /dev/null
@@ -1,3 +0,0 @@
-# These are commented until we enable lint and typecheck
-# npx tsc -p tsconfig.json --noEmit
-# npx lint-staged
\ No newline at end of file
diff --git a/.node-version b/.node-version
deleted file mode 100644
index f94d3c2e..00000000
--- a/.node-version
+++ /dev/null
@@ -1 +0,0 @@
-24.13.1
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
index 036ee8bc..bc876b6c 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -3,4 +3,4 @@ node_modules
package.json
package-lock.json
LICENSE
-harness
+README.md
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 859eab14..fea421b2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,162 +2,43 @@
First off, thanks for taking the time to contribute! β€οΈ
-This document describes the process of contributing to Cinny. It is intended
-for anyone considering opening an **issue**, **discussion** or **pull request**.
-For people who are interested in developing Cinny and technical details behind
-it, please check out our ["Developing Cinny"](HACKING.md) document as well.
+All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. π
-> And if you like the project, but just don't have time to contribute, that's fine.
-> There are other easy ways to support the project and show your
-> appreciation, which we would also be very happy about:
->
+> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> - Star the project
> - Tweet about it (tag @cinnyapp)
> - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues
> - [Donate to us](https://cinny.in/#sponsor)
-## The Critical Rule
+## Bug reports
-**The most important rule: you must understand your code.** If you can't
-explain what your changes do and how they interact with the greater system, do not contribute to this project.
+Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/ajbura/cinny/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.
-## AI Usage
-
-The Cinny project stictly does not allow any AI generated code. Any pull request containing it will be rejected. **This is very important.**
-
-## Quick Guide
-
-### I'd like to contribute
+## Pull requests
> ### Legal Notice
->
-> When contributing to this project, you must agree that you have authored 100%
-> of the content, that you have the necessary rights to the content and that
-> the content you contribute may be provided under the project license. You will
-> also be asked to [sign the CLA] ([why CLA?]) upon submiting your pull request.
+> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
-[All issues are actionable](#issues-are-actionable). Pick one and start
-working on it. Thank you. If you need help or guidance, comment on the issue.
-Issues that are extra friendly to new contributors are tagged with
-["contributor friendly"].
+**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.
-["contributor friendly"]: https://github.com/cinnyapp/cinny/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22contributor%20friendly%22
-[sign the cla]: https://github.com/cinnyapp/cla
-[why CLA?]: https://github.com/cinnyapp/cla#but-why
+**Please use clean, concise titles for your pull requests.** We use commit squashing, so the final commit in the dev branch will carry the title of the pull request. For easier sorting in changelog, start your pull request titles using one of the verbs "Add", "Change", "Remove", or "Fix" (present tense).
-### I have a bug! / Something isn't working
+Example:
-First, search the issue tracker and discussions for similar issues. Tip: also
-search for [closed issues] and [discussions] β your issue might have already
-been fixed!
+|Not ideal|Better|
+|---|----|
+|Fixed markAllAsRead in RoomTimeline|Fix read marker when paginating room timeline|
-> [!NOTE]
->
-> If there is an _open_ issue or discussion that matches your problem,
-> **please do not comment on it unless you have valuable insight to add**.
->
-> GitHub has a very _noisy_ set of default notification settings which
-> sends an email to _every participant_ in an issue/discussion every time
-> someone adds a comment. Instead, use the handy upvote button for discussions,
-> and/or emoji reactions on both discussions and issues, which are a visible
-> yet non-disruptive way to show your support.
+It is not always possible to phrase every change in such a manner, but it is desired.
-If your issue hasn't been reported already, open an ["Issue Triage"] discussion
-and make sure to fill in the template **completely**. They are vital for
-maintainers to figure out important details about your setup.
+**The smaller the set of changes in the pull request is, the quicker it can be reviewed and merged.** Splitting tasks into multiple smaller pull requests is often preferable.
-> [!WARNING]
->
-> A _very_ common mistake is to file a bug report either as a Q&A or a Feature
-> Request. **Please don't do this.** Otherwise, maintainers would have to ask
-> for your system information again manually, and sometimes they will even ask
-> you to create a new discussion because of how few detailed information is
-> required for other discussion types compared to Issue Triage.
->
-> Because of this, please make sure that you _only_ use the "Issue Triage"
-> category for reporting bugs β thank you!
+Also, we use [ESLint](https://eslint.org/) for clean and stylistically consistent code syntax, so make sure your pull request follow it.
-[closed issues]: https://github.com/cinnyapp/cinny/issues?q=is%3Aissue%20state%3Aclosed
-[discussions]: https://github.com/cinnyapp/cinny/discussions?discussions_q=is%3Aclosed
-["issue triage"]: https://github.com/cinnyapp/cinny/discussions/new?category=issue-triage
+**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
-### I have an idea for a feature
-
-Like bug reports, first search through both issues and discussions and try to
-find if your feature has already been requested. Otherwise, open a discussion
-in the ["Feature Requests, Ideas"] category.
-
-["feature requests, ideas"]: https://github.com/cinnyapp/cinny/discussions/new?category=feature-requests-ideas
-
-### I've implemented a feature
-
-1. If there is an issue for the feature, open a pull request straight away.
-2. If there is no issue, open a discussion and link to your branch.
-3. If you want to live dangerously, open a pull request and
- [hope for the best](#pull-requests-implement-an-issue).
-
-### I have a question which is neither a bug report nor a feature request
-
-Open an [Q&A discussion], or join our [Matrix Space] and ask away in the
-`Cinny Support` room.
-
-Do not use other rooms to ask for help as our rooms are mostly specific
-topic only. If you do ask a question there, you will be redirected
-to `Cinny Support` room instead.
-
-> [!NOTE]
-> If your question is about a missing feature, please open a discussion under
-> the ["Feature Requests, Ideas"] category. If Cinny is behaving
-> unexpectedly, use the ["Issue Triage"] category.
->
-> The "Q&A" category is strictly for other kinds of discussions and do not
-> require detailed information unlike the two other categories, meaning that
-> maintainers would have to spend the extra effort to ask for basic information
-> if you submit a bug report under this category.
->
-> Therefore, please **pay attention to the category** before opening
-> discussions to save us all some time and energy. Thank you!
-
-[q&a discussion]: https://github.com/cinnyapp/cinny/discussions/new?category=q-a
-[matrix space]: https://matrix.to/#/#cinny:matrix.org
-
-## General Patterns
-
-### Issues are Actionable
-
-> [!NOTE]
-> This is WIP and only issue with number greater than #2876 are actionable.
-
-The Cinny [issue tracker](https://github.com/cinnyapp/cinny/issues)
-is for _actionable items_.
-
-Unlike some other projects, Cinny **does not use the issue tracker for
-discussion or feature requests**. Instead, we use GitHub
-[discussions](https://github.com/cinnyapp/cinny/discussions) for that.
-Once a discussion reaches a point where a well-understood, actionable
-item is identified, it is moved to the issue tracker. **This pattern
-makes it easier for maintainers or contributors to find issues to work on
-since _every issue_ is ready to be worked on.**
-
-If you are experiencing a bug and have clear steps to reproduce it, please
-open an issue. If you are experiencing a bug but you are not sure how to
-reproduce it or aren't sure if it's a bug, please open a discussion.
-If you have an idea for a feature, please open a discussion.
-
-### Pull Requests Implement an Issue
-
-Pull requests should be associated with a previously accepted issue.
-**If you open a pull request for something that wasn't previously discussed,**
-it may be closed or remain stale for an indefinite period of time. I'm not
-saying it will never be accepted, but the odds are stacked against you.
-
-Issues tagged with "feature" represent accepted, well-scoped feature requests.
-If you implement an issue tagged with feature as described in the issue, your
-pull request will be accepted with a high degree of certainty.
-
-> [!NOTE]
->
-> **Pull requests are NOT a place to discuss feature design.** Please do
-> not open a WIP pull request to discuss a feature. Instead, use a discussion
-> and link to your branch.
+## Helpful links
+- [BEM methodology](http://getbem.com/introduction/)
+- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
+- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
diff --git a/Dockerfile b/Dockerfile
index c09f7861..46d0db6e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
## Builder
-FROM node:24.13.1-alpine AS builder
+FROM node:20.12.2-alpine3.18 as builder
WORKDIR /src
@@ -11,7 +11,7 @@ RUN npm run build
## App
-FROM nginx:1.31.5-alpine
+FROM nginx:1.29.1-alpine
COPY --from=builder /src/dist /app
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
diff --git a/HACKING.md b/HACKING.md
deleted file mode 100644
index 30c4960f..00000000
--- a/HACKING.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# Developing Cinny
-
-> [!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-windows]
-> on Windows and [nvm] on Linux/macOS are pretty good choices. Recommended
-> nodejs version is Krypton LTS (v24.13.1).
-
-[nvm-windows]: https://github.com/coreybutler/nvm-windows#installation--upgrades
-[nvm]: https://github.com/nvm-sh/nvm
-
-Execute the following commands to start a development server:
-
-```sh
-npm ci # Installs all dependencies
-npm start # Serve a development version
-```
-
-To build the app:
-
-```sh
-npm run build # Compiles the app into the dist/ directory
-```
-
-To commit changes:
-
-```sh
-npm run commit
-```
-
-## Running with Docker
-
-This repository includes a Dockerfile, which builds the application from
-source and serves it with Nginx on port 80. To use this locally, you can
-build the container like so:
-
-```
-docker build -t cinny:latest .
-```
-
-You can then run the container you've built with a command similar to this:
-
-```
-docker run -p 8080:80 cinny:latest
-```
-
-This will forward your `localhost` port 8080 to the container's port 80.
-You can visit the app in your browser by navigating to `http://localhost:8080`.
-
-## Code formatting
-
-We use [ESLint](https://eslint.org/) for clean and stylistically
-consistent code syntax, so make sure your pull request follow it.
-
-## Helpful links
-
-- [BEM methodology](http://getbem.com/introduction/)
-- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
-- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
diff --git a/README.md b/README.md
index 46c7a67b..427898f9 100644
--- a/README.md
+++ b/README.md
@@ -12,64 +12,27 @@
-A Matrix client focusing primarily on simple, elegant and secure interface.
-The main goal is to have an instant messaging application that is easy on
-people and has a modern touch.
+A Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have an instant messaging application that is easy on people and has a modern touch.
- [Roadmap](https://github.com/orgs/cinnyapp/projects/1)
- [Contributing](./CONTRIBUTING.md)
-> [!IMPORTANT]
-We are currently in the process of [replacing] the matrix-js-sdk with our
-own SDK. As a result, we will not be accepting any pull requests until
-further notice. Thank you for your understanding.
-
-[replacing]: https://github.com/cinnyapp/cinny/issues/257#issuecomment-3714406704
-
## Getting started
-The web app is available at [app.cinny.in] and gets updated on each new
-release. The `dev` branch is continuously deployed at [dev.cinny.in]
-but keep in mind that it could have things broken.
+The web app is available at [app.cinny.in](https://app.cinny.in/) and gets updated on each new release. The `dev` branch is continuously deployed at [dev.cinny.in](https://dev.cinny.in) but keep in mind that it could have things broken.
-You can also download our desktop app from the [cinny-desktop repository].
-
-[app.cinny.in]: https://app.cinny.in
-[dev.cinny.in]: https://dev.cinny.in
-[cinny-desktop repository]: https://github.com/cinnyapp/cinny-desktop
-
-## Contributing and Developing
-
-If you have any ideas, issues, etc. regarding Cinny, or would like to
-contribute to Cinny through pull requests, please check out our
-["Contributing to Cinny"](CONTRIBUTING.md) document. Those who would like
-to get involved with Cinny's development as well should also read the
-["Developing Cinny"](HACKING.md) document for more technical details.
+You can also download our desktop app from the [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
## Self-hosting
-To host Cinny on your own, simply download the tarball from
-[GitHub releases], and serve the files from `dist/` using your preferred
-webserver. Alternatively, you can just pull the docker image from
-[DockerHub] or [GitHub Container Registry].
+To host Cinny on your own, simply download the tarball from [GitHub releases](https://github.com/cinnyapp/cinny/releases/latest), and serve the files from `dist/` using your preferred webserver. Alternatively, you can just pull the docker image from [DockerHub](https://hub.docker.com/r/ajbura/cinny) or [GitHub Container Registry](https://github.com/cinnyapp/cinny/pkgs/container/cinny).
* The default homeservers and explore pages are defined in [`config.json`](config.json).
-* You need to set up redirects to serve the assests. Example configurations;
-[netlify](netlify.toml), [nginx](contrib/nginx/cinny.domain.tld.conf),
-[caddy](contrib/caddy/caddyfile).
- * If you have trouble configuring redirects you can
- [enable hash routing](config.json#L35) β the url in the browser will have
- a `/#/` between the domain and open channel (ie. `app.cinny.in/#/home/`
- instead of `app.cinny.in/home/`) but you won't have to configure your webserver.
+* You need to set up redirects to serve the assests. Example configurations; [netlify](netlify.toml), [nginx](contrib/nginx/cinny.domain.tld.conf), [caddy](contrib/caddy/caddyfile).
+ * If you have trouble configuring redirects you can [enable hash routing](config.json#L35) β the url in the browser will have a `/#/` between the domain and open channel (ie. `app.cinny.in/#/home/` instead of `app.cinny.in/home/`) but you won't have to configure your webserver.
-* To deploy on subdirectory, you need to rebuild the app youself after
-updating the `base` path in [`build.config.ts`](build.config.ts).
- * For example, if you want to deploy on `https://cinny.in/app`, then
- set `base: '/app'`.
-
-[GitHub releases]: https://github.com/cinnyapp/cinny/releases/latest
-[DockerHub]: https://hub.docker.com/r/ajbura/cinny
-[GitHub Container Registry]: https://github.com/cinnyapp/cinny/pkgs/container/cinny
+* To deploy on subdirectory, you need to rebuild the app youself after updating the `base` path in [`build.config.ts`](build.config.ts).
+ * For example, if you want to deploy on `https://cinny.in/app`, then set `base: '/app'`.
PGP Public Key to verify tarball
@@ -118,11 +81,31 @@ mxFo+ioe/ABCufSmyqFye0psX3Sp
```
-## License
-Cinny Project
-Copyright Β© 2024βpresent Ajay Bura
-https://cinny.in
+## Local development
+> [!TIP]
+> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Iron LTS (v20).
-Cinny is licensed under the GNU Affero General Public License,
-Version 3 of the License (AGPL-3.0-only).
-You may obtain a copy of the License at https://www.gnu.org/licenses/agpl-3.0.html
\ No newline at end of file
+Execute the following commands to start a development server:
+```sh
+npm ci # Installs all dependencies
+npm start # Serve a development version
+```
+
+To build the app:
+```sh
+npm run build # Compiles the app into the dist/ directory
+```
+
+### Running with Docker
+This repository includes a Dockerfile, which builds the application from source and serves it with Nginx on port 80. To
+use this locally, you can build the container like so:
+```
+docker build -t cinny:latest .
+```
+
+You can then run the container you've built with a command similar to this:
+```
+docker run -p 8080:80 cinny:latest
+```
+
+This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by navigating to `http://localhost:8080`.
diff --git a/build.config.ts b/build.config.ts
index ac51db97..ec8a41d0 100644
--- a/build.config.ts
+++ b/build.config.ts
@@ -1,3 +1,3 @@
export default {
- base: process.env.CINNY_BASE ?? '/',
+ base: '/',
};
diff --git a/config.json b/config.json
index e945d1c4..de6015a1 100644
--- a/config.json
+++ b/config.json
@@ -1,17 +1,34 @@
{
- "defaultHomeserver": 1,
- "homeserverList": ["converser.eu", "matrix.org", "mozilla.org", "unredacted.org", "xmr.se"],
+ "defaultHomeserver": 2,
+ "homeserverList": [
+ "converser.eu",
+ "envs.net",
+ "matrix.org",
+ "monero.social",
+ "mozilla.org",
+ "xmr.se"
+ ],
"allowCustomHomeservers": true,
"featuredCommunities": {
"openAsDefault": false,
- "spaces": ["#gamecult:gamecult.org"],
- "rooms": [
- "#tuwunel:grin.hu",
- "#freesoftware:matrix.org",
- "#gentoo:matrix.org"
+ "spaces": [
+ "#cinny-space:matrix.org",
+ "#community:matrix.org",
+ "#space:envs.net",
+ "#science-space:matrix.org",
+ "#libregaming-games:tchncs.de",
+ "#mathematics-on:matrix.org"
],
- "servers": ["matrixrooms.info", "matrix.org", "mozilla.org", "unredacted.org"]
+ "rooms": [
+ "#cinny:matrix.org",
+ "#freesoftware:matrix.org",
+ "#pcapdroid:matrix.org",
+ "#gentoo:matrix.org",
+ "#PrivSec.dev:arcticfoxes.net",
+ "#disroot:aria-net.org"
+ ],
+ "servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
},
"hashRouter": {
diff --git a/docs/gamecult/ratatoskr-target.md b/docs/gamecult/ratatoskr-target.md
deleted file mode 100644
index 166c5c72..00000000
--- a/docs/gamecult/ratatoskr-target.md
+++ /dev/null
@@ -1,352 +0,0 @@
-# Ratatoskr: target and prose map
-
-Ratatoskr is GameCult's Matrix chat client: this fork of Cinny, forge
-`GameCult/cinny` (to be renamed `GameCult/Ratatoskr`; see the cut sequence).
-Void stewards it. Campaign `ratatoskr` lives in the eureka mind for now, beside
-the rulings it rests on. Under `eureka-body:ruling:ruling-home-steward-mind` it
-is handed to Void's mind once, by procedure, as soon as that hand-off procedure
-exists (`hand_off` is parked); after that every new ruling on it is admitted in
-Void's mind. Nothing here is written into Void's mind before then.
-
-The mind holds the questions, rulings, specs, reports and verdicts. This file
-holds what no document kind carries: the design truths, body facts, the model
-page, rationale, the reconstructed collaboration plan, and the cut sequence.
-All times are UTC.
-
-## Why
-
-GameCult is the only server this client talks to. A general Matrix client
-treats it as one community among thousands, so the operator meets GameCult's
-structure three clicks deep in an invite list of 50+ rooms (operator, `#swarm`
-10-31 05:14: "The biggest pain is navigation. GameCult has a structure."). The
-client should open on GameCult's estate as data: its spaces, its project rooms
-grouped by steward, its Minds, and the person's own place in it. It should
-also be a pleasant place to be in its own right: emotes, sound, and weather in
-the background.
-
-## Design truths (from rulings in force)
-
-- **We own the interface, not the engine.** The fork keeps Cinny's engine
- (matrix-js-sdk, Rust crypto, sync) and replaces the whole UI; every
- user-facing string and its translation is ours, and upstream locale files
- are not the source (`gamecult-chat:ruling:we-own-the-ui-and-translation`).
-- **Mobile first.** On phones, inactive elements hide, vertical space is
- cheaper than horizontal, and text never gets squeezed into narrow columns
- (`eureka-body:ruling:chat-client-base-cinny-mobile-first`).
-- **The first spec** (`eureka-body:ruling:chat-client-first-spec`): an emote
- slot on every bubble (a pose id carried on the event); room and participant
- lists tiered (category, then items) and reached from the header menu, not
- side columns; participants grouped by estate (Minds, Members, Users,
- Patrons); rooms grouped by kind (general, estates, projects, work rooms);
- Minds indexed by stewardship, read from Matrix state projected from the
- stewardship records in the Minds' own minds (the minds stay the truth); one
- room per project, a Space child, with the steward joined.
-- **Rooms are visible without a join dance.** No invite screen in the way; a
- room the server lets you join shows in the same list as joined ones; opening
- one shows its name and a one-tap Join; joining happens on that tap or a first
- post, never silently; no peeking (operator, `#swarm` 10-31 04:08, 04:12
- "Peeking sounds like YAGNI to me", 04:14 "One tap is a reasonable cost").
- Rooms you may not enter are refused by the server, not hidden by the client
- (Heimdall 04:10, accepted by Void 04:11 and 04:15).
-- **Language.** A user's language is the BCP 47 tag in profile field
- `org.gamecult.language`, owned by the user through the client's language
- picker (`gamecult-chat:ruling:user-language-profile-field`). The client ships
- localized; people talk with stewards in their own language; rooms are made
- for a language, with no per-language mirrors
- (`gamecult-chat:ruling:multilingual-client-stewards-language-rooms`). One
- host Mind sits in a language room, Weksa for `#pt-br`
- (`gamecult-chat:ruling:language-room-one-host-weksa`). The first person this
- must work for is a non-English, non-technical patron (placeholder
- `patron-pt1`) whose account we set up and walk through login
- (`gamecult-chat:ruling:patron-in-his-own-language`).
-- **Push.** `gamecult-chat:ruling:chat-push` rules out native mobile push for
- now; Web Push to the installed web app is a different thing and is open
- (question `ratatoskr:question:pwa-web-push`).
-- **Collaboration is recorded, not chatted.** Work spanning Minds belongs in
- Atlas (`eureka-body:ruling:atlas-records-collaborative-work`). Atlas is not
- deployed, so the plan below is its interim home; Atlas imports it later.
-
-## Body facts
-
-Probes run by Imagination on 2026-10-31, session `self-2026-10-29-eureka`.
-
-1. **Branch.** `void/gamecult-theme-and-project-nav` is `b8e696c8` over main
- `1e6412ed` (v4.12.8), 2 commits, 14 files, +1153/-32 (686 of it lockfile):
- a GameCult theme as default, member drawer hidden by default, bubble
- layout, and steward grouping in `src/app/pages/client/space/Space.tsx`
- (`useStewardGroups` at 429-437, group headers at 505-524) over
- `useSpaceJoinedHierarchy` (409). Read by `git diff`.
-2. **Live estate shape** (Synapse on Yggdrasil, read as `admin` over the
- client API, counts only). Root Space `#gamecult:gamecult.org`, room version
- 12, has 9 children: 8 rooms and the `Projects` sub-Space. `Projects` has 52
- children; 50 readable rooms, all room version 12. Server `/hierarchy` from
- the root returns 59 rooms, 2 of them spaces. **Every room's join rule is
- `invite`**; none is world-readable. 50 rooms carry `org.gamecult.steward`
- with content `{mind, repo, source, steward: }`. The root carries
- `org.gamecult.stewardships`: Mind localpart to list of `Org/Repo`.
-3. **Consequence of 1 and 2.** Project rooms are invite-only and Members are
- invited, not joined (gamecult-ops `runbooks/gamecult-chat-host-yggdrasil.md`
- 146-158: "Members and every Mind are invited; the steward alone joins").
- So a Member's project rooms are pending invites. Void's grouping reads
- `useSpaceJoinedHierarchy` and a per-room state event that an invitee cannot
- read, so for a typical Member it groups nothing and the rooms stay in the
- invite list. This is the operator's "three clicks deep" complaint, seen from
- the data.
-4. **Provisioning contract.** gamecult-ops
- `scripts/synapse-accounts-hand-managed.py` writes the rooms and both state
- events from the Minds' stewardship documents; each project room's alias is
- `#` (runbook 146-150). This is the only place the client
- can learn a not-yet-joined room's steward today: hierarchy `canonical_alias`
- joined to the root `org.gamecult.stewardships`. Follow-up
- `ratatoskr:follow_up:stewardships-carry-room-ids` asks the projection to
- name room ids so the client stops leaning on the alias convention.
-5. **Sync and storage** (`src/client/initMatrix.ts`). The client uses
- matrix-js-sdk 41.7.0 with `IndexedDBStore` (`web-sync-store`, 15-19) and the
- Rust crypto store, and `startClient({ lazyLoadMembers: true })` (43-47): v2
- `/sync`, no sliding sync, no custom filter or `initialSyncLimit`. A cold
- start is therefore an IndexedDB load plus an incremental sync from the
- stored token, not a full initial sync.
-6. **Server sync support.** `/_matrix/client/versions` on Synapse 1.164.1
- (127.0.0.1:8834) advertises `org.matrix.simplified_msc3575: true`:
- simplified sliding sync (MSC4186) is available. Cinny 4.12.8 does not use it.
-7. **Shell delivery.** `https://chat.gamecult.org/assets/index-DWIvG3QT.js` is
- served at 3,736,520 bytes with no `Content-Encoding` even when gzip and br
- are offered, and with no `Cache-Control` (only `ETag`/`Last-Modified`). The
- service worker (`src/sw.ts`, 158 lines) handles only `install`, `activate`,
- a session `message`, and authenticated media `fetch` (79-158); the
- `vite-plugin-pwa` config sets `injectionPoint: undefined`
- (`vite.config.ts` 101-113), so nothing is precached.
-8. **No push path.** `src/sw.ts` has no `push` or `notificationclick` handler;
- no code subscribes through `PushManager`; the only pusher the client sets is
- the email pusher (`src/app/features/settings/notifications/SystemNotification.tsx`
- 22-40). Notifications and sounds fire only from a live page:
- `src/app/pages/client/ClientNonUIFeatures.tsx` 79-129 (invites) and 131-258
- (messages: `RoomEvent.Timeline` at 181-235, `new window.Notification` at
- 156, `notification.ogg` played at 175-178). Nothing in `src/` listens for
- `visibilitychange` or calls `retryImmediately`.
-9. **Sygnal WebPush** (matrix-org/sygnal `docs/applications.md`, read
- 2026-10-31; the page itself is marked partly out of date): a `webpush` app
- takes `vapid_private_key`, `vapid_contact_email`, optional
- `allowed_endpoints` and `ttl`; the client's pusher `data` carries
- `endpoint`, `auth`, `p256dh` and optional `default_payload`; the push
- carries `room_id`, `room_name`, `sender_display_name`, `content`, unread
- counts and so on; browsers need a visible notification for each push
- (`events_only` avoids event-less pushes).
-
-10. **Baseline at `dd88424e`** (Yggdrasil, `ygg-verify.sh`, image
- `node:22-bookworm`, `npm ci --ignore-scripts`): `vitest run` passes (1
- file, 5 tests, Void's steward grouping); `vite build` succeeds, `dist/` 61
- MB with source maps, main chunk 3,739,716 bytes, Rust crypto wasm
- 5,573,870 bytes, `dist/sw.mjs` 1.66 kB. `tsc --noEmit` fails at base with
- 793 errors in 246 files (455 of them TS2614, named imports from
- `matrix-js-sdk` 41.7.0 not resolved), so typecheck is not a usable gate as
- is; a cut gates on zero errors in the files it adds and no rise in the
- total.
-
-Not probed: phone frame times, the real resume timeline on the operator's
-Android Chrome install (needs her device or remote DevTools), the Rust crypto
-wasm size, and the router base under `/preview/` (its `config.json` sets
-`hashRouter.basename` to `/`).
-
-## Why the PWA resumes slowly (diagnosis, moderate confidence)
-
-Android freezes a backgrounded installed web app and later discards it. On a
-**discard**, returning is a full cold boot: 3.7 MB of uncompressed JS over a
-phone link with no precache (fact 7), Rust crypto wasm init, the IndexedDB
-store load, then a catch-up `/sync` over every room since the stored token
-(fact 5). On a **freeze without discard**, the in-flight long-poll is dead, and
-the SDK only notices on its own timers because the client never nudges it on
-`visibilitychange` (fact 8). The catch-up sync grows with the number of rooms
-and the swarm's chat volume, which is exactly where sliding sync helps
-(fact 6), but the first two costs are cheaper to remove and should be measured
-first. Without Web Push, a frozen page cannot notify at all, which is the
-second half of the operator's report.
-
-## Model page: the estate
-
-| Kind | Named by | Over time | Who decides |
-|---|---|---|---|
-| Stewardship | `:stewardship:.n` in each Mind's mind | sequence advances on hand-over | the operator and the Minds; the minds are the truth |
-| Estate projection | `org.gamecult.stewardships` on the root Space; `org.gamecult.steward` per project room | re-PUT on every provisioning run | gamecult-ops provisioning script (Eureka's slice) |
-| Estate room list | the server's `/hierarchy` of the root Space for this user | changes with Space children and the user's invites and joins | Synapse (membership and join rules); the client derives |
-| Room membership | the user's `m.room.member` | join on tap or first post | the user |
-| Navigator view | derived per render | never stored | the client's estate index |
-
-## Authority map for the navigator
-
-- **Owner:** one estate index in the client (`src/app/features/estate/`):
- `buildEstate()` is pure, `useEstate()` feeds it. It decides what rooms the
- person sees, in which tier and group, and with what membership affordance.
-- **Inputs:** the paged server `/hierarchy` of the root Space; root Space
- state `org.gamecult.stewardships`; the person's membership per room from the
- client store; unread and selection atoms for emphasis only; the `m.direct`
- list for the direct-messages tier.
-- **Outputs:** an ordered list of tiers (one per root room group and per
- sub-Space, plus Direct), each holding steward groups or rooms, each room
- marked joined, invited or joinable.
-- **Derived:** steward group labels (Mind display names); counts; collapsed
- state is per-device UI state only.
-- **Demoted or dead:** the sidebar rail and its account-data ordering
- (`useSidebarItems`) are dead; `useSpaceJoinedHierarchy` no longer decides
- what is listed; Void's per-room steward reader is deleted, not kept beside
- the index.
-- **Shared paths:** every nav slot (home, direct, space routes) renders the
- same navigator; tapping Join and sending a first post both go through
- `mx.joinRoom`; deep links into a room still resolve through the existing
- room routes.
-
-## Reconstructed collaboration plan (Atlas import)
-
-Planned in `#swarm` and `#minds` on 2026-10-31 and then lost to scroll. Status
-as of 2026-10-31 ~15:00.
-
-**Who holds which part**
-
-| Part | Holder | Status | Evidence |
-|---|---|---|---|
-| The client: integration, navigator, emote slot, sound hookup, background canvas | Void (steward) | theme and steward grouping on branch; nothing else built | `void:stewardship:gamecult_-cinny.n1`; `#swarm` 10-31 14:54 Void report |
-| Dings earcons: motif per event kind, one stable instrument per Mind | Aqua | not rendered: render env blocked, then .NET SDK installed and Faust scouted; AquaSynth PR #1 (Linux Faust loader, `faust-loader-platform-aware`) merged at 3b43b7bd | `aqua-body:follow_up:chat-s410-416`, `aqua-body:follow_up:chat-s410-416-render-env`, `aqua-body:resolution:follow_up.chat-s430-430.n1`, forge AquaSynth pull 1 |
-| Ambient soundscape for the client | Aqua | idea only; no design recorded | operator `#swarm` 10-31 08:32 ("Motifs for everything"), `eureka-body:ruling:atlas-records-collaborative-work` |
-| Background math, in CultMath: the public `fbm` gap, Phacelle erosion's Lipschitz bound, any new GLSL mirrors | Libby (steward of CultLib, which owns CultMath; holds `cultmath-tapes`) | sources located; nothing built | `#swarm` 10-31 06:34 and 06:46 Libby; `libby-body:follow_up:cultmath-ts-noise-absent`; `#swarm` 04:22 Libby accepts `cultmath-tapes` |
-| Background scene and render: the raymarched terrain, the tree-and-squirrel intro scene, lighting and atmosphere, march and TAA tuning on the site-ground framework | Nibu (steward of Aetheria, whose volumetric nebula and cloud shaders she owns: `Raymarching/CloudShader.shader`, `Volumetric.cginc`) | proposed by the operator; not yet taken up by Nibu | operator to Self, 10-31: "If we want to spread the work around, Nibu owns the nebulae so she has shader expertise." |
-| Animated bubbles, flourishes, corner avatar and XP bar | Void, with Kiko's offer on juice | advice only | `#swarm` 10-31 03:59 Kiko and Void, 06:45 Void |
-| Attention bubbles | Eureka (Huginn heartbeat publishes), Void (render), Heimdall (scope rules) | advice only | `#swarm` 10-31 04:00 Eureka, 04:02 Heimdall |
-
-**Agreements**
-
-- Dings are pre-rendered clips shipped as static assets, no synth in the
- browser: `dings//.ogg` plus a small manifest; an
- unknown sender falls back to a neutral ding; ogg or mp3, not wav (Void
- `#minds` 03:19, Aqua 07:41). The set is motif times instrument, and the
- instrument says who spoke (operator 03:18, Eureka 03:19). Events that sound:
- a mention, a DM, an invite, a message in an unmuted room; mentions and DMs
- loud, ordinary messages quiet; everything else silent (Void `#minds` 03:19).
- The operator wants far more than four motifs (08:32). The instrument per
- Mind belongs in a typed document on the Mind side, not hardcoded (Eureka
- 03:19, advice). Aqua's ear check comes before anything is called done, and
- the operator's ear is ground truth (Aqua 07:41). Browsers block audio until
- the first click, so the first ding after a fresh load may be silent.
-- The background has its own canvas and budget, separate from the bubble
- flourishes; capped at 30 fps, paused when hidden; under reduced motion one
- static frame; an adaptive step-down (Aqua 04:17, Void 04:18 and 04:22).
-- The background is an infinite raymarched terrain: CultMath noise plus the
- Phacelle erosion filter, in the shader (operator 04:18 "painter's algorithm",
- 05:24 "all we need is noise", 05:25 erosion, restated by the operator to
- Self on 10-31: "use CultMath noise and erosion filter running in the shader
- to have an infinite terrain. We can just raymarch that instead"). Voronoi
- and Apollonian are dropped (05:24).
-- Yggdrasil is the scene's centre (operator to Self, 10-31: "Yggdrasil should
- be the star of the show now, so really the camera should just pull back a
- bit when you login and the personas should be orbiting the tree, dancing
- around each other in accordance with their activity"). On login the camera
- pulls back from the tree; there is no separate intro to cut away from. The
- Personas orbit the tree and move around each other by their live activity.
-- One activity vocabulary drives both sound and motion: Aqua's activity-kinds
- grammar (`aqua-body:follow_up:chat-s431-432`, "enumerate the kinds of agent
- activity in typed state") is the shared contract between Aqua's motifs and
- Nibu's orbits, not two vocabularies. The live feed behind it is the same
- typed state the swarm dashboard projects
- (`eureka-body:ruling:swarm-dashboard-first-thing2-surface`, Libby;
- `libby-body:follow_up:chat-s354-359`): Run documents, the heartbeat trace.
- What reaches the client is a member-safe projection: kind, Mind, time and
- intensity, never briefs or mind contents. Matrix presence and typing cover
- humans and are too coarse for Minds. Where the feed comes from is
- `ratatoskr:question:activity-feed-source`.
-- The scene renders in two passes, not one loop (operator to Self, 10-31:
- "The tape optimized solid surface march should run really fast, i imagine,
- way faster than the volumetric version, so they should not be one loop").
- Pass 1, the solid-surface march (terrain, tree, squirrel), sphere-traces
- with the field's Lipschitz bound and interval tile culling, at its own
- resolution, and writes depth. Pass 2, the volumetric atmosphere march on
- the site-ground framework (reduced resolution, TAA), is bounded by pass 1's
- depth, so it marches only up to the surface hit. Each pass has its own
- budget and its own measurement. Nibu owns both passes.
-- The web math path is CultLib's own: HLSL source lowered by dxc and
- SPIRV-Cross to WebGL2 GLSL ES (`packages/cultmath/shaders/CultMath.glsl`,
- `.Phacelle.glsl`; `tools/glsl/build-glsl.sh`), and C# CPU math through
- .NET wasm, with parity presumed for now (operator 06:23, 06:48; Libby
- 06:34). No TS or WGSL port. The gamecult.org site ground already
- concatenates `CultMath.glsl` in `ground.js` and runs an interval-culled,
- TAA-reprojected per-pixel march (CultLib `docs/cultmath-interval-ground-cut.md`,
- spec `cultmath-interval-ground-cut-site-ground.r3`): that framework is the
- base, not new tape work.
-- Gaps go to the owner: a plain public `fbm` (only `fbm_grad`, `span_fbm`,
- `af_fbm` exist) is added in CultMath, not in the client
- (`libby-body:follow_up:cultmath-ts-noise-absent`; Libby 07:57 "tell me what
- you need and I'll fill it at the library").
-- Mood and flourish ride as metadata on the event, never guessed from text;
- bounded per-frame budget; reduced-motion fallback (Void 03:59, Kiko 03:59).
-- XP is two numbers, never one: a cosmetic, labelled game bar fed by things
- worth doing, and contribution credit shown only once a real measure exists
- (Void 06:45, with Nibu and Kiko).
-
-**Open items**
-
-- Libby: the raymarch step bound needs the field's Lipschitz bound. CultMath
- tracks `SNOISE_LIPSCHITZ` for noise; erosion's bound is unknown and must be
- probed before the march is trusted.
-- Libby: public `fbm` in CultMath, with its GLSL mirror.
-- Nibu (once she takes it): the terrain scene, lighting and atmosphere, and
- march and TAA tuning on the site ground's framework at a phone budget;
- waits on Libby's `fbm` and the erosion bound. Then the intro scene in the
- same world.
-- Aqua: render the first earcon set once Faust builds in her sandbox; then the
- instrument-per-Mind list and the manifest; then the soundscape design.
-- Aqua and Nibu: agree the activity vocabulary once (Aqua's grammar), so
- motifs and orbits read the same kinds.
-- Void: the navigator (next cut); the Dings hook at
- `ClientNonUIFeatures.tsx` 131-258; the background canvas after Libby's shader.
-- Eureka: compression and caching for the shell on the chat host
- (`ratatoskr:follow_up:chat-shell-compression`); the stewardship projection's
- room ids; a Web Push gateway if the operator picks it.
-
-## Cut sequence
-
-`estate-navigator` is specced (`ratatoskr:cut_spec:cut-estate-navigator.r1`).
-Each later cut is one line, with what it waits on.
-
-- `resume-fast`: precache the app shell in the service worker, nudge sync on
- `visibilitychange`, and measure cold and warm resume on the operator's phone
- before and after; waits on nothing; runs beside `estate-navigator` in its
- own worktree, and pairs with the ops follow-up on compression.
-- `pwa-web-push`: service worker `push` and `notificationclick` handlers, a
- `webpush` pusher registered with VAPID keys, and a self-hosted Sygnal on the
- chat host; waits on `ratatoskr:question:pwa-web-push`.
-- `sliding-sync`: move the client to simplified sliding sync (MSC4186), which
- the server already offers; waits on `resume-fast`'s measurements showing
- catch-up sync dominates.
-- `dings`: per-Mind earcons through the manifest at the existing notification
- hook, and the same motifs in push notifications; waits on Aqua's renders.
-- `emote-slot`: the pose-id emote on every bubble and the picker; waits on
- `estate-navigator` (shared header work).
-- `participant-tiers`: participants tiered by estate in the header menu, Minds
- indexed by stewardship; waits on `estate-navigator`.
-- `language`: our own strings and translations, the `org.gamecult.language`
- picker, and the walk-through login for `patron-pt1`; waits on
- `estate-navigator`.
-- `terrain-background`: the raymarched CultMath noise and Phacelle terrain on
- the site ground's march and TAA framework, in its own canvas and budget, as
- two passes (solid-surface sphere trace writing depth, then the volumetric
- atmosphere bounded by it), each measured on a phone; Nibu on the scene and
- both passes, Libby's CultMath underneath, Void
- integrating; waits on Libby's `fbm` and the erosion Lipschitz probe (both in
- CultLib), then a phone measurement.
-- `yggdrasil-scene`: the tree and squirrel at the centre of the terrain world;
- on login the camera pulls back from the tree, and it may play during the
- cold start or resume sync but never holds the client up; Nibu on the scene,
- Libby on any CultMath it needs, Void integrating, Aqua's soundscape under
- it; waits on `terrain-background`.
-- `orbiting-personas`: Personas orbit the tree and dance around each other by
- live activity, from the shared activity vocabulary; waits on
- `yggdrasil-scene`, Aqua's activity-kinds grammar, and
- `ratatoskr:question:activity-feed-source`.
-- `bubbles-and-juice`: animated bubbles and mood flourishes from event
- metadata, the corner avatar and the cosmetic XP bar; waits on `emote-slot`.
-- `attention-bubbles`: per-person attention bubbles from Huginn's typed
- attention document, scoped to room members; waits on Huginn publishing it.
-- `rename-ratatoskr`: package name, app name and branding, manifest and
- titles, the `/preview/` path and its deploy, and every in-repo `cinny`
- reference that is ours (not upstream engine paths); waits on Void's forge
- rename of `GameCult/cinny` to `GameCult/Ratatoskr`
- (`eureka-body:ruling:chat-client-named-ratatoskr`), and the campaign's repo
- list moves with it.
diff --git a/harness/estate/check.cjs b/harness/estate/check.cjs
deleted file mode 100644
index 656d532e..00000000
--- a/harness/estate/check.cjs
+++ /dev/null
@@ -1,46 +0,0 @@
-// Assertions for the estate drives: every drive states the outcome it exists to show and exits
-// non-zero when the page or the fake homeserver's log says otherwise.
-const fs = require('fs');
-const fails = [];
-exports.expect = (label, ok, detail = '') => {
- console.log(`[ASSERT] ${ok ? 'PASS' : 'FAIL'} ${label}${ok ? '' : ` :: ${detail}`}`);
- if (!ok) fails.push(label);
-};
-/** Exit code for the drive: 4 when any assertion failed. */
-exports.done = () => (fails.length ? 4 : 0);
-/** The fake homeserver's request log (HSLOG is shared with the server process). */
-exports.hsLog = () => fs.readFileSync(process.env.HSLOG, 'utf8').split('\n');
-/** Bodies of the account-data PUTs for m.direct, in order. */
-exports.mDirectPuts = () =>
- exports.hsLog().filter((l) => / PUT .*m\.direct BODY /.test(l)).map((l) => l.split(' BODY ')[1]);
-/** Join requests the server saw for a room id. */
-exports.joinsOf = (roomId) => exports.hsLog().filter((l) => new RegExp(` JOIN ${roomId}$`).test(l)).length;
-const HEADERS = ['GameCult', 'Projects', 'nibu', 'eureka', 'Nibu', 'Eureka', 'Elsewhere', 'Direct Messages'];
-/** The tier header above a row, 'absent' when the page does not show the row, 'none-above' when no tier header precedes it. */
-const tierOf = (pg, name) =>
- pg.evaluate((n) => {
- const s = document.body.innerText.split('\n').map((x) => x.trim());
- const i = s.indexOf(n);
- if (i < 0) return 'absent';
- for (let j = i; j >= 0; j--) if (/^(elsewhere|direct messages)$/i.test(s[j])) return s[j];
- return 'none-above';
- }, name);
-/**
- * The tier a room is listed under, read from a fresh visit to the room list: a collapsed tier is
- * opened only if the row is not already showing, so reading never closes what is open.
- */
-exports.revealedTier = async (pg, name) => {
- await pg.goto('http://127.0.0.1:8080/#/home/');
- await pg.waitForTimeout(2500);
- let t = await tierOf(pg, name);
- for (const h of HEADERS) {
- if (t !== 'absent') break;
- const hl = pg.getByText(h, { exact: true });
- if (await hl.count()) { await hl.first().click(); await pg.waitForTimeout(250); t = await tierOf(pg, name); }
- }
- return t;
-};
-/** True when the page text matches re within ms of now (a message can be replaced by a row change). */
-exports.sawText = (pg, re, ms) =>
- pg.waitForFunction((src) => new RegExp(src, 'i').test(document.body.innerText), re.source, { timeout: ms, polling: 50 }).then(() => true, () => false);
-exports.isDm = (tier) => /^direct messages$/i.test(tier);
diff --git a/harness/estate/drive-failures.cjs b/harness/estate/drive-failures.cjs
deleted file mode 100644
index d53d88a4..00000000
--- a/harness/estate/drive-failures.cjs
+++ /dev/null
@@ -1,72 +0,0 @@
-// Failure-mode scenarios for the estate join paths (estate tap and inbox Accept), run by run.sh
-// against fakehs.cjs with the env flag each mode needs.
-const { chromium } = require('playwright');
-const { expect, done, isDm, mDirectPuts, joinsOf, revealedTier, sawText } = require('./check.cjs');
-const MODE = process.env.MODE;
-const T0 = Date.now(); const t = () => `+${Date.now() - T0}ms`;
-const log = (...a) => console.log(`[${MODE}]`, ...a);
-const HEADERS = ['GameCult', 'Projects', 'nibu', 'eureka', 'Nibu', 'Eureka', 'Elsewhere', 'Direct Messages'];
-let b;
-// Close the browser, but never wait on it: a hung close must not hang the run.
-const quit = (code) => Promise.race([Promise.resolve(b && b.close()).catch(() => undefined), new Promise((r) => setTimeout(r, 5000))]).finally(() => process.exit(code));
-setTimeout(() => { console.log('DRIVER_TIMEOUT'); quit(3); }, 240000);
-(async () => {
- b = await chromium.launch();
- const ctx = await b.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true, deviceScaleFactor: 1 });
- const pg = await ctx.newPage(); pg.on('console', (m) => { if (m.type() === 'error') console.log('[console]', m.type(), m.text().slice(0, 160)); });
- const open = async (h = '#/home/') => { await pg.goto('http://127.0.0.1:8080/' + h); await pg.reload(); await pg.waitForTimeout(9000); };
- await pg.goto('http://127.0.0.1:8080/');
- await pg.evaluate(() => { localStorage.setItem('cinny_access_token', 'tok'); localStorage.setItem('cinny_device_id', 'DEV'); localStorage.setItem('cinny_user_id', '@t:test'); localStorage.setItem('cinny_hs_base_url', 'http://127.0.0.1:8080'); });
- await open();
- const body = async () => (await pg.evaluate(() => document.body.innerText)).replace(/\s+/g, ' ');
- const reveal = async (name) => { const loc = pg.locator(`[aria-label="Join ${name}"]`); if (await loc.count()) return loc; for (let r = 0; r < 2; r++) for (const h of HEADERS) { const hl = pg.getByText(h, { exact: true }); if (await hl.count()) { await hl.first().click(); await pg.waitForTimeout(250); if (await loc.count()) return loc; } } return loc; };
- const tierOf = (name) => revealedTier(pg, name);
- const tap = async (n) => { const l = await reveal(n); log('JOINCTRL', n, await l.count()); await l.first().click(); log('CLICK', n, t()); };
- const tiers = async (tag) => { const r = { friend: await tierOf('Friend DM'), slow: await tierOf('Slow DM') }; log('TIER', tag, '| Friend DM', r.friend, '| Slow DM', r.slow); return r; };
- const neverUnfiled = () => mDirectPuts().every((b) => b.includes('!dm1:test'));
- const acceptInInbox = async (name) => {
- // reach the inbox through the navigator's own Inbox tab
- await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.waitForTimeout(2000);
- const tab = pg.locator('[aria-label="Inbox"], [aria-label*="nbox"]'); log('INBOX_TAB', await tab.count());
- if (await tab.count()) { await tab.first().click(); await pg.waitForTimeout(1500); }
- const inv = pg.getByText('Invites', { exact: true }); if (await inv.count()) { await inv.first().click(); await pg.waitForTimeout(1500); }
- log('URL', pg.url());
- if (!/invites/.test(pg.url())) { await pg.goto('http://127.0.0.1:8080/#/inbox/invites/'); await pg.waitForTimeout(3000); log('URL_FALLBACK', pg.url()); }
- { const pub = pg.getByText('Public', { exact: true }); log('PUBLIC_FILTER', await pub.count()); if (await pub.count()) { await pub.first().click(); await pg.waitForTimeout(1500); } }
- for (let r = 0; r < 3; r++) { const v = pg.getByText('View All', { exact: true }); if (!(await v.count())) break; await v.first().click(); await pg.waitForTimeout(800); }
- const b0 = await body(); const i = b0.indexOf(name); log('INBOX has', name, i >= 0, JSON.stringify(b0.slice(Math.max(0, i - 120), i + 120)));
- const card = pg.locator('div').filter({ hasText: name }).filter({ has: pg.getByText('Accept', { exact: true }) }).last();
- await card.getByText('Accept', { exact: true }).first().click(); log('ACCEPT', name, t());
- };
- if (MODE === 'pjfail') {
- await tap('Friend DM'); await pg.waitForTimeout(3000);
- const bb = await body(); const k = bb.search(/Internal server error/); log('AFTER_FAIL', k >= 0 ? bb.slice(Math.max(0, k - 40), k + 80) : 'no message');
- expect('a failed m.direct write shows the refusal', k >= 0, bb.slice(0, 200));
- expect('no join is sent while the filing is failing', joinsOf('!dm1:test') === 0, String(joinsOf('!dm1:test')));
- await tap('Friend DM'); await pg.waitForTimeout(6000); const r = await tiers('after retry');
- expect('the retry joins once', joinsOf('!dm1:test') === 1, String(joinsOf('!dm1:test')));
- expect('Friend DM is filed under Direct Messages after the retry', isDm(r.friend), r.friend);
- } else if (MODE === 'jdrop') {
- await tap('Friend DM'); const saw = await sawText(pg, /tap again to retry/, 3000); log('AFTER_DROP', saw);
- expect('a dropped join shows the retryable message', saw, (await body()).slice(0, 200));
- await pg.waitForTimeout(6000); const r1 = await tiers('after drop'); await open(); const r2 = await tiers('after reload');
- expect('the entry is kept after the drop', isDm(r1.friend) && neverUnfiled(), r1.friend);
- expect('Friend DM is under Direct Messages after the reload', isDm(r2.friend), r2.friend);
- } else if (MODE === 'admin') {
- await tap('Friend DM'); await pg.waitForTimeout(6000); const r = await tiers('admin estate');
- expect('an admin-invited DM is filed under Direct Messages', isDm(r.friend), r.friend);
- } else if (MODE === 'inboxadmin') {
- await acceptInInbox('Friend DM'); await pg.waitForTimeout(6000); const r = await tiers('admin inbox');
- expect('an admin-invited DM accepted in the inbox is filed under Direct Messages', isDm(r.friend), r.friend);
- } else if (MODE === 'inboxreload') {
- await acceptInInbox('Slow DM'); await pg.waitForTimeout(2000); log('RELOAD', t()); await open('#/inbox/invites/');
- await pg.waitForTimeout(30000); const r = await tiers('inbox reload');
- expect('a DM accepted in the inbox is filed under Direct Messages after a reload', isDm(r.slow), r.slow);
- } else if (MODE === 'pjreload') {
- await tap('Slow DM'); await pg.waitForTimeout(2000); log('RELOAD', t()); await open();
- await pg.waitForTimeout(8000); await tiers('pjreload');
- const n = await (await reveal('Slow DM')).count(); log('SLOW_DM_STILL_JOINABLE', n);
- expect('a join cut off by a reload stays joinable', n === 1, String(n));
- }
- await quit(done());
-})().catch((e) => { log('ERROR', String(e.stack || e).slice(0, 300)); quit(1); });
diff --git a/harness/estate/drive-routes.cjs b/harness/estate/drive-routes.cjs
deleted file mode 100644
index 75c9a8df..00000000
--- a/harness/estate/drive-routes.cjs
+++ /dev/null
@@ -1,42 +0,0 @@
-const { chromium } = require('playwright');
-const { expect, done, isDm, mDirectPuts, hsLog, revealedTier, sawText } = require('./check.cjs');
-const MODE = process.env.MODE; const T0 = Date.now(); const t = () => `+${Date.now() - T0}ms`;
-const log = (...a) => console.log(`[${MODE}]`, ...a);
-const HEADERS = ['GameCult', 'Projects', 'nibu', 'eureka', 'Nibu', 'Eureka', 'Elsewhere', 'Direct Messages'];
-let b; const quit = (c) => Promise.race([Promise.resolve(b && b.close()).catch(() => 0), new Promise((r) => setTimeout(r, 5000))]).finally(() => process.exit(c));
-setTimeout(() => { console.log('DRIVER_TIMEOUT'); quit(3); }, 120000);
-(async () => {
- b = await chromium.launch();
- const ctx = await b.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
- const pg = await ctx.newPage();
- await pg.goto('http://127.0.0.1:8080/');
- await pg.evaluate(() => { localStorage.setItem('cinny_access_token', 'tok'); localStorage.setItem('cinny_device_id', 'DEV'); localStorage.setItem('cinny_user_id', '@t:test'); localStorage.setItem('cinny_hs_base_url', 'http://127.0.0.1:8080'); });
- await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.reload(); await pg.waitForTimeout(9000);
- const body = async () => (await pg.evaluate(() => document.body.innerText)).replace(/\s+/g, ' ');
- const tiers = async (tag) => { const f = await revealedTier(pg, 'Friend DM'); log('TIER', tag, '| Friend DM', f); return f; };
- let joined = false;
- if (MODE === 'deeplink') {
- for (const route of ['direct', 'home']) {
- await pg.goto(`http://127.0.0.1:8080/#/${route}/${encodeURIComponent('!dm1:test')}/`); await pg.waitForTimeout(4000);
- const bb = await body(); log('ROUTE', route, JSON.stringify(bb.slice(0, 300)));
- const j = pg.getByRole('button', { name: /^Join$/ }); log('JOIN_BUTTONS', route, await j.count());
- if (await j.count()) { joined = true; await j.first().click(); log('CLICK_JOIN', route, t()); await pg.waitForTimeout(6000); break; }
- }
- const f = await tiers('after deeplink join');
- const stamps = (re) => hsLog().filter((l) => re.test(l)).map((l) => Number(l.split(' ')[0]));
- const put = stamps(/ PUT .*m\.direct BODY .*!dm1:test/)[0], join = stamps(/ JOIN !dm1:test$/)[0];
- expect('the deep link offers one Join button', joined, 'no Join button');
- expect('the DM is filed before the join is sent', put !== undefined && join !== undefined && put <= join, `put ${put} join ${join}`);
- expect('the DM is filed under Direct Messages', isDm(f), f);
- } else if (MODE === 'gw504') {
- const HL = HEADERS; let l = pg.locator('[aria-label="Join Friend DM"]');
- for (let r = 0; r < 2 && !(await l.count()); r++) for (const h of HL) { const hl = pg.getByText(h, { exact: true }); if (await hl.count()) { await hl.first().click(); await pg.waitForTimeout(250); } }
- log('JOINCTRL', await l.count()); await l.first().click(); log('CLICK', t());
- const saw = await sawText(pg, /tap again to retry/, 3000); log('AFTER_504', saw);
- expect('a gateway timeout shows the retryable message', saw, (await body()).slice(0, 200));
- await pg.waitForTimeout(4000); const f = await tiers('after 504 (join delivered by sync)');
- expect('the DM stays filed after a gateway timeout', mDirectPuts().every((b) => b.includes('!dm1:test')) && mDirectPuts().length > 0, JSON.stringify(mDirectPuts()));
- expect('the DM is filed under Direct Messages once sync delivers it', isDm(f), f);
- }
- await quit(done());
-})().catch((e) => { log('ERROR', String(e.stack || e).slice(0, 300)); quit(1); });
diff --git a/harness/estate/drive.cjs b/harness/estate/drive.cjs
deleted file mode 100644
index 03bd2ad0..00000000
--- a/harness/estate/drive.cjs
+++ /dev/null
@@ -1,48 +0,0 @@
-const { chromium } = require('playwright');
-const { expect, done, isDm, revealedTier } = require('./check.cjs');
-const MODE = process.env.MODE; // twodm | reload
-const T0 = Date.now(); const t = () => `+${Date.now() - T0}ms`;
-const log = (...a) => console.log(`[${MODE}]`, ...a);
-const HEADERS = ['GameCult', 'Projects', 'nibu', 'eureka', 'Nibu', 'Eureka', 'Elsewhere', 'Direct Messages'];
-let tierBefore;
-(async () => {
- const b = await chromium.launch();
- const ctx = await b.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true, deviceScaleFactor: 1 });
- const pg = await ctx.newPage(); pg.on('console', (m) => { if (m.type() === 'error' || /command|direct/i.test(m.text())) console.log('[console]', m.type(), m.text().slice(0, 200)); });
- const open = async () => { await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.reload(); await pg.waitForTimeout(9000); };
- await pg.goto('http://127.0.0.1:8080/');
- await pg.evaluate(() => { localStorage.setItem('cinny_access_token', 'tok'); localStorage.setItem('cinny_device_id', 'DEV'); localStorage.setItem('cinny_user_id', '@t:test'); localStorage.setItem('cinny_hs_base_url', 'http://127.0.0.1:8080'); });
- await open();
- const text = () => pg.evaluate(() => document.body.innerText);
- const reveal = async (name) => { const loc = pg.locator(`[aria-label="Join ${name}"]`); if (await loc.count()) return loc; for (let r = 0; r < 2; r++) for (const h of HEADERS) { const hl = pg.getByText(h, { exact: true }); if (await hl.count()) { await hl.first().click(); await pg.waitForTimeout(250); if (await loc.count()) return loc; } } return loc; };
- const tierOf = (name) => revealedTier(pg, name);
- const tap = async (n) => { const l = await reveal(n); log('JOINCTRL', n, await l.count()); await l.first().click(); log('CLICK', n, t()); };
- if (MODE === 'twodm') {
- await tap('Slow DM'); await pg.waitForTimeout(300); await tap('Late DM');
- await pg.waitForTimeout(45000);
- } else if (MODE === 'convert') {
- await tap('Friend DM'); await pg.waitForTimeout(6000);
- tierBefore = await tierOf('Friend DM'); log('TIER before', tierBefore);
- await pg.goto('http://127.0.0.1:8080/#/direct/!dm1:test/'); await pg.waitForTimeout(4000);
- const ed = pg.locator('[contenteditable="true"]'); log('EDITORS', await ed.count(), pg.url());
- await ed.first().click(); await pg.keyboard.type('/converttoroom', { delay: 30 }); await pg.waitForTimeout(500);
- log('ED_TEXT', JSON.stringify(await ed.first().innerText())); log('BODY', JSON.stringify((await text()).slice(-600)));
- await pg.getByText('Convert direct message to room').first().click(); await pg.waitForTimeout(400); log('ED_PICKED', JSON.stringify(await ed.first().innerHTML()).slice(0, 300));
- await pg.keyboard.press('Enter'); log('SENT', t()); await pg.waitForTimeout(800); log('ED_TEXT2', JSON.stringify(await ed.first().innerText().catch(()=>'')));
- if (await ed.first().innerText().catch(() => '')) { await pg.keyboard.press('Enter'); log('SENT2', t()); }
- await pg.waitForTimeout(8000);
- await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.waitForTimeout(3000);
- } else {
- await tap('Slow DM'); await pg.waitForTimeout(3000);
- log('RELOAD', t()); await open();
- await pg.waitForTimeout(30000);
- }
- const slow = await tierOf('Slow DM'), late = await tierOf('Late DM'), friend = await tierOf('Friend DM');
- log('TIER Slow DM', slow, '| Late DM', late, '| Friend DM', friend);
- if (MODE === 'twodm') { expect('Slow DM is filed under Direct Messages', isDm(slow), slow); expect('Late DM is filed under Direct Messages', isDm(late), late); }
- else if (MODE === 'convert') {
- expect('Friend DM was filed under Direct Messages before the conversion', isDm(tierBefore), tierBefore);
- expect('the converted room left Direct Messages and is still listed', !isDm(friend) && friend !== 'absent', friend);
- } else expect('Slow DM is filed under Direct Messages after the reload', isDm(slow), slow);
- await b.close(); process.exitCode = done();
-})().catch((e) => { log('ERROR', String(e.stack || e)); process.exitCode = 1; });
diff --git a/harness/estate/fakehs.cjs b/harness/estate/fakehs.cjs
deleted file mode 100644
index 4a6875e0..00000000
--- a/harness/estate/fakehs.cjs
+++ /dev/null
@@ -1,127 +0,0 @@
-// Soul s2 fake homeserver. Invitee with no root state; stewards only in m.space.child content.
-// Rooms: GEN joined; OP invited; 6 project rooms invited (odin slow: join arrives at +24 s);
-// PUB public joinable, not in store; STRAY joined outside hierarchy; DM1 invited direct (fast);
-// DM2 invited direct (slow, +24 s). NOROOT=1: the root alias does not resolve.
-const http = require('http'), fs = require('fs'), path = require('path');
-const DIST = process.argv[2], PORT = 8080, ME = '@t:test';
-const NOROOT = process.env.NOROOT === '1';
-const log = [];
-const P = ['aetheria', 'ghostlight', 'huginn', 'idunn', 'bifrost', 'odin'];
-const STEWARD = { aetheria: 'nibu', ghostlight: 'nibu', huginn: 'eureka', idunn: 'eureka' };
-const PROJ = P.map((p, i) => ({ id: `!p${i}:test`, alias: `#${p}:test`, name: p[0].toUpperCase() + p.slice(1), key: p }));
-const ROOT = '!root:test', PSP = '!proj:test', GEN = '!gen:test', OP = '!op:test', PUB = '!pub:test', STRAY = '!stray:test', DM1 = '!dm1:test', DM2 = '!dm2:test', DM3 = '!dm3:test';
-const SLOW = new Set([PROJ[5].id, DM2, DM3]);
-const NAMES = { [PUB]: 'Commons', [DM1]: 'Friend DM', [DM2]: 'Slow DM', [DM3]: 'Late DM', [STRAY]: 'Adhoc' };
-PROJ.forEach((r) => (NAMES[r.id] = r.name));
-const held = []; const pendingState = []; const pending = []; const adLog = []; let ev = 0;
-const se = (type, content, sk = '', sender = '@admin:test') => ({ type, content, state_key: sk, sender, event_id: `$e${++ev}`, origin_server_ts: Date.now() });
-const strip = (id, name, space, direct) => [
- { type: 'm.room.create', state_key: '', sender: direct ? '@friend:test' : '@admin:test', content: space ? { type: 'm.space', room_version: '12' } : { room_version: '12' } },
- { type: 'm.room.name', state_key: '', sender: '@admin:test', content: { name } },
- { type: 'm.room.join_rules', state_key: '', sender: '@admin:test', content: { join_rule: 'invite' } },
- ...(direct ? [{ type: 'm.room.member', state_key: '@friend:test', sender: '@friend:test', content: { membership: 'join', displayname: 'Friend' } }] : []),
- { type: 'm.room.member', state_key: ME, sender: direct ? (process.env.INVITER_ADMIN === '1' && id === DM1 ? '@admin:test' : '@friend:test') : '@admin:test', content: direct ? { membership: 'invite', is_direct: true } : { membership: 'invite' } },
-];
-const joinedRoom = (id, name, extra = [], space = false) => ({
- state: { events: [se('m.room.create', space ? { type: 'm.space', room_version: '12' } : { room_version: '12' }), se('m.room.name', { name }), se('m.room.join_rules', { join_rule: 'invite' }), se('m.room.member', { membership: 'join' }, ME, ME), ...extra] },
- timeline: { events: [{ type: 'm.room.message', sender: '@admin:test', content: { msgtype: 'm.text', body: `welcome to ${name}` }, event_id: `$m${++ev}`, origin_server_ts: Date.now() }], limited: false },
- ephemeral: { events: [] }, account_data: { events: [] }, unread_notifications: {}, summary: {},
-});
-const invites = () => {
- const o = {};
- o[ROOT] = { invite_state: { events: strip(ROOT, 'GameCult', true) } };
- o[PSP] = { invite_state: { events: strip(PSP, 'Projects', true) } };
- o[OP] = { invite_state: { events: strip(OP, 'Operator') } };
- PROJ.forEach((r) => (o[r.id] = { invite_state: { events: strip(r.id, r.name) } }));
- o[DM1] = { invite_state: { events: strip(DM1, NAMES[DM1], false, true) } };
- o[DM2] = { invite_state: { events: strip(DM2, NAMES[DM2], false, true) } };
- o[DM3] = { invite_state: { events: strip(DM3, NAMES[DM3], false, true) } };
- return o;
-};
-const child = (id, steward) => ({ type: 'm.space.child', state_key: id, content: { via: ['test'], ...(steward ? { 'org.gamecult.steward': { mind: steward, repo: 'x', source: 'y', steward: 'z' } } : {}) }, sender: '@admin:test', origin_server_ts: 1 });
-const hroom = (id, name, alias, extra = {}) => ({ room_id: id, name, canonical_alias: alias, num_joined_members: 1, world_readable: false, guest_can_join: false, join_rule: 'invite', children_state: [], ...extra });
-const hierarchy = () => [
- hroom(ROOT, 'GameCult', '#gamecult:test', { room_type: 'm.space', children_state: [child(GEN), child(OP), child(PUB), child(PSP)] }),
- hroom(GEN, 'General', '#general:test'),
- hroom(OP, 'Operator', '#operator:test'),
- hroom(PUB, 'Commons', '#commons:test', { join_rule: 'public' }),
- hroom(PSP, 'Projects', '#projects:test', { room_type: 'm.space', children_state: PROJ.map((r) => child(r.id, STEWARD[r.key])) }),
- ...PROJ.map((r) => hroom(r.id, r.name, r.alias)),
-];
-let syncN = 1;
-const json = (res, code, body) => { res.writeHead(code, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Methods': '*' }); res.end(JSON.stringify(body)); };
-const mime = { '.js': 'text/javascript', '.mjs': 'text/javascript', '.css': 'text/css', '.html': 'text/html', '.json': 'application/json', '.wasm': 'application/wasm', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff2': 'font/woff2' };
-const flush = () => fs.writeFileSync(process.env.HSLOG, log.join('\n'));
-http.createServer((req, res) => {
- const u = new URL(req.url, 'http://x'); const p = decodeURIComponent(u.pathname);
- if (req.method === 'OPTIONS') return json(res, 200, {});
- if (!p.startsWith('/_matrix') && !p.startsWith('/.well-known')) {
- let f = path.join(DIST, p === '/' ? 'index.html' : p);
- if (!fs.existsSync(f) || fs.statSync(f).isDirectory()) f = path.join(DIST, 'index.html');
- res.writeHead(200, { 'Content-Type': mime[path.extname(f)] || 'application/octet-stream' });
- return fs.createReadStream(f).pipe(res);
- }
- let body = ''; req.on('data', (c) => (body += c)); req.on('end', () => {
- if (req.method === 'PUT' && p.includes('m.direct')) console.log('MDIRECT_PUT ' + (Date.now()) + ' ' + body);
- log.push(`${Date.now()} ${req.method} ${p}${req.method === 'PUT' && p.includes('account_data') ? ' BODY ' + body : ''}`);
- if (p === '/_matrix/client/versions') return json(res, 200, { versions: ['v1.1', 'v1.5', 'v1.11'], unstable_features: {} });
- if (p.endsWith('/sync')) {
- const since = u.searchParams.get('since');
- if (!since) {
- const join = {}; join[GEN] = joinedRoom(GEN, 'General'); join[STRAY] = joinedRoom(STRAY, 'Adhoc');
- return json(res, 200, { next_batch: `s${syncN}`, rooms: { join, invite: invites(), leave: {} }, account_data: { events: held.slice() }, presence: { events: [] }, to_device: { events: [] }, device_one_time_keys_count: { signed_curve25519: 50 } });
- }
- const respond = () => {
- const now = Date.now(); const join = {};
- for (let i = pending.length - 1; i >= 0; i--) if (pending[i].at <= now) {
- const id = pending[i].id; pending.splice(i, 1);
- const extra = id === DM1 || id === DM2 || id === DM3 ? [se('m.room.member', { membership: 'join', displayname: 'Friend' }, '@friend:test', '@friend:test')] : [];
- join[id] = joinedRoom(id, NAMES[id] || id, extra); if (extra.length) { const me = join[id].state.events.find((e) => e.type === 'm.room.member' && e.state_key === ME); me.unsigned = { prev_content: { membership: 'invite', is_direct: true } }; } log.push(`${now} SYNC delivers join ${id}`); console.log('DELIVER ' + now + ' ' + id);
- }
- for (const s of pendingState.splice(0)) { join[s.id] = join[s.id] || { state: { events: [] }, timeline: { events: [], limited: false, prev_batch: 'p' }, ephemeral: { events: [] }, account_data: { events: [] }, unread_notifications: {}, summary: {} }; join[s.id].timeline.events.push(s.ev); console.log('STATE_ECHO ' + now + ' ' + s.id + ' ' + JSON.stringify(s.ev.content)); }
- const k = Number(String(since).replace(/^s/, '')) || 0; const ad = adLog.filter((a) => a.after >= k).map((a) => ({ type: a.type, content: a.content })); // a server answers with what changed since the client's token, whichever client asks
- if (Object.keys(join).length || ad.length) return json(res, 200, { next_batch: `s${++syncN}`, rooms: { join, invite: {}, leave: {} }, account_data: { events: ad } });
- return json(res, 200, { next_batch: since, rooms: {} });
- };
- return setTimeout(respond, 300);
- }
- const jm = p.match(/\/rooms\/([^/]+)\/join$/) || p.match(/\/join\/([^/]+)$/);
- if (req.method === 'POST' && jm) {
- const id = jm[1]; log.push(`${Date.now()} JOIN ${id}`);
- if (id === PROJ[1].id) return json(res, 403, { errcode: 'M_FORBIDDEN', error: 'You are not invited to this room.' });
- const prior = pending.find((q) => q.id === DM2 || q.id === DM3);
- pending.push({ id, at: (id === DM3 || id === DM2) && prior ? prior.at : Date.now() + (SLOW.has(id) ? 24000 : 1000) });
- if (process.env.JOIN_504 === '1' && id === DM1) { console.log('JOIN_504 ' + Date.now() + ' ' + id); res.writeHead(504, {'Content-Type':'text/html'}); return res.end('504 Gateway Time-out'); }
- if (process.env.JOIN_DROP === '1' && id === DM1) { console.log('JOIN_DROPPED ' + Date.now() + ' ' + id); return req.socket.destroy(); }
- if (process.env.JOIN_DELAY === '1') { console.log('JOIN_DELAYED ' + Date.now() + ' ' + id); return setTimeout(() => json(res, 200, { room_id: id }), 6000); }
- return json(res, 200, { room_id: id });
- }
- const sm = p.match(/\/rooms\/([^/]+)\/state\/m\.room\.member\/(.+)$/);
- if (req.method === 'PUT' && sm) { console.log('STATE_PUT ' + Date.now() + ' ' + sm[1] + ' ' + body); const e = se('m.room.member', JSON.parse(body), sm[2], sm[2]); e.unsigned = { prev_content: { membership: 'join' } }; setTimeout(() => pendingState.push({ id: sm[1], ev: e }), 400); return json(res, 200, { event_id: e.event_id }); }
- if (p.includes('/account_data/') && req.method === 'PUT' && p.includes('m.direct') && process.env.FAIL_MDIRECT === '1' && !global.failedOnce) { global.failedOnce = 1; console.log('MDIRECT_FAIL ' + Date.now()); return json(res, 500, { errcode: 'M_UNKNOWN', error: 'Internal server error' }); }
- if (p.includes('/account_data/') && req.method === 'PUT') {
- const DELAY = p.includes('m.direct') && process.env.DELAY_MDIRECT === '1';
- const type = decodeURIComponent(p.split('/account_data/')[1]);
- try { const c = JSON.parse(body); adLog.push({ type, content: c, after: syncN }); const i = held.findIndex((h) => h.type === type); if (i >= 0) held.splice(i, 1); held.push({ type, content: c }); } catch (e) {}
- if (DELAY) { console.log('MDIRECT_DELAYED ' + Date.now()); return setTimeout(() => json(res, 200, {}), 6000); }
- return json(res, 200, {});
- }
- if (p.includes('/hierarchy')) return json(res, 200, { rooms: hierarchy() });
- if (p.includes('/directory/room/')) return NOROOT ? json(res, 404, { errcode: 'M_NOT_FOUND', error: 'Room alias not found' }) : json(res, 200, { room_id: ROOT, servers: ['test'] });
- if (p.endsWith('/keys/upload')) return json(res, 200, { one_time_key_counts: { signed_curve25519: 50 } });
- if (p.endsWith('/keys/query')) return json(res, 200, { device_keys: {}, failures: {} });
- if (p.includes('/room_keys/version')) return json(res, 404, { errcode: 'M_NOT_FOUND', error: 'none' });
- if (p.includes('/pushrules')) return json(res, 200, { global: { override: [], underride: [], room: [], sender: [], content: [] } });
- if (p.includes('/filter')) return json(res, 200, { filter_id: '1' });
- if (p.includes('/account_data/')) return json(res, 404, { errcode: 'M_NOT_FOUND', error: 'none' });
- if (p.includes('/messages')) return json(res, 200, { chunk: [], start: 't0', end: 't0' });
- if (p.includes('/members')) return json(res, 200, { chunk: [] });
- if (p.includes('/profile/')) return json(res, 200, { displayname: 'Tess' });
- if (p.includes('/capabilities')) return json(res, 200, { capabilities: {} });
- if (p.includes('/devices')) return json(res, 200, { devices: [] });
- if (p.includes('summary')) return json(res, 200, { room_id: p.split('/').pop(), name: 'Summary card', num_joined_members: 1, membership: 'invite', join_rule: 'invite' });
- if (p.includes('/login')) return json(res, 200, { flows: [{ type: 'm.login.password' }] });
- return json(res, 200, {});
- });
-}).listen(PORT, () => console.log('fakehs up'));
-setInterval(flush, 300);
diff --git a/harness/estate/run.sh b/harness/estate/run.sh
deleted file mode 100644
index 65242ab0..00000000
--- a/harness/estate/run.sh
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env bash
-# Runs inside mcr.microsoft.com/playwright:v1.48.2-jammy with the repo at /src:
-# builds the client, then drives the direct-message and join-failure scenarios against fakehs.cjs.
-set -e
-cd /src
-npm ci --no-audit --no-fund --ignore-scripts >/tmp/npmci.log 2>&1 || { tail -20 /tmp/npmci.log; exit 1; }
-npm run build > /tmp/build.log 2>&1 || { tail -30 /tmp/build.log; exit 1; }
-echo BUILD_OK $(git rev-parse HEAD)
-printf '{"defaultHomeserver":0,"homeserverList":["127.0.0.1:8080"],"allowCustomHomeservers":true,"featuredCommunities":{"spaces":["#gamecult:test"],"rooms":[],"servers":["test"]},"hashRouter":{"enabled":true,"basename":"/"}}' > dist/config.json
-mkdir -p /tmp/pw && cd /tmp/pw && npm init -y >/dev/null && npm i --no-audit --no-fund playwright@1.48.2 >/tmp/pw.log 2>&1
-cp /src/harness/estate/check.cjs /src/harness/estate/drive.cjs /src/harness/estate/drive-failures.cjs /src/harness/estate/drive-routes.cjs /tmp/pw/
-set +e
-for m in twodm reload convert; do
- HSLOG=/tmp/hslog-$m.txt NOROOT=0 node /src/harness/estate/fakehs.cjs /src/dist > /tmp/hs-$m.out 2>&1 &
- HS=$!; sleep 2
- HSLOG=/tmp/hslog-$m.txt MODE=$m node /tmp/pw/drive.cjs; rc=$?; echo "DRIVE $m RC=$rc"; [ $rc -eq 0 ] || FAILED="$FAILED $m"
- kill $HS; wait $HS 2>/dev/null; sleep 1
- echo "=== hs $m"; grep -E "MDIRECT_PUT|DELIVER|STATE_" /tmp/hs-$m.out; grep -E " JOIN " /tmp/hslog-$m.txt
-done
-failure() { m=$1; shift
- env "$@" HSLOG=/tmp/hslog-$m.txt NOROOT=0 node /src/harness/estate/fakehs.cjs /src/dist > /tmp/hs-$m.out 2>&1 &
- HS=$!; sleep 2
- HSLOG=/tmp/hslog-$m.txt MODE=$m node /tmp/pw/${DRIVER:-drive-failures}.cjs; rc=$?; echo "DRIVE $m RC=$rc"; [ $rc -eq 0 ] || FAILED="$FAILED $m"
- kill $HS; wait $HS 2>/dev/null; sleep 1
- echo "=== hs $m"; grep -E "MDIRECT|DELIVER|JOIN_|STATE_" /tmp/hs-$m.out; grep -E " JOIN " /tmp/hslog-$m.txt; }
-failure pjfail FAIL_MDIRECT=1
-failure pjreload DELAY_MDIRECT=1
-failure jdrop JOIN_DROP=1
-failure admin INVITER_ADMIN=1
-failure inboxadmin INVITER_ADMIN=1
-failure inboxreload JOIN_DELAY=1
-DRIVER=drive-routes failure deeplink
-DRIVER=drive-routes failure gw504 JOIN_504=1
-echo "FAILED_DRIVES:${FAILED:- none}"
-[ -z "$FAILED" ]
diff --git a/package-lock.json b/package-lock.json
index dc774000..4abfac35 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,21 +1,18 @@
{
"name": "cinny",
- "version": "4.12.8",
+ "version": "4.10.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cinny",
- "version": "4.12.8",
+ "version": "4.10.0",
"license": "AGPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
- "@fontsource/ibm-plex-mono": "5.2.5",
"@fontsource/inter": "4.5.14",
- "@fontsource/montserrat": "5.2.5",
- "@fontsource/ubuntu": "5.2.5",
"@tanstack/react-query": "5.24.1",
"@tanstack/react-query-devtools": "5.24.1",
"@tanstack/react-virtual": "3.2.0",
@@ -35,19 +32,18 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
- "folds": "2.7.2",
+ "folds": "2.5.0",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
"i18next-browser-languagedetector": "8.0.0",
- "i18next-http-backend": "4.0.3",
+ "i18next-http-backend": "2.5.2",
"immer": "9.0.16",
"is-hotkey": "0.2.0",
"jotai": "2.6.0",
- "linkify-react": "4.3.2",
- "linkifyjs": "4.3.2",
- "matrix-js-sdk": "41.7.0",
- "matrix-widget-api": "1.16.1",
+ "linkify-react": "4.1.3",
+ "linkifyjs": "4.1.3",
+ "matrix-js-sdk": "38.2.0",
"millify": "6.1.0",
"pdfjs-dist": "4.2.67",
"prismjs": "1.30.0",
@@ -60,16 +56,15 @@
"react-google-recaptcha": "2.1.0",
"react-i18next": "15.0.0",
"react-range": "1.8.14",
- "react-router-dom": "6.30.6",
- "sanitize-html": "2.18.1",
- "slate": "0.123.0",
- "slate-dom": "0.123.0",
- "slate-history": "0.113.1",
- "slate-react": "0.123.0",
+ "react-router-dom": "6.20.0",
+ "sanitize-html": "2.12.1",
+ "slate": "0.112.0",
+ "slate-dom": "0.112.2",
+ "slate-history": "0.110.3",
+ "slate-react": "0.112.1",
"ua-parser-js": "1.0.35"
},
"devDependencies": {
- "@element-hq/element-call-embedded": "0.20.1",
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@rollup/plugin-inject": "5.0.3",
"@rollup/plugin-wasm": "6.1.1",
@@ -81,13 +76,12 @@
"@types/react": "18.2.39",
"@types/react-dom": "18.2.17",
"@types/react-google-recaptcha": "2.1.8",
- "@types/sanitize-html": "2.16.1",
+ "@types/sanitize-html": "2.9.0",
"@types/ua-parser-js": "0.7.36",
"@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1",
"@vitejs/plugin-react": "4.2.0",
"buffer": "6.0.3",
- "cz-conventional-changelog": "3.3.0",
"eslint": "8.29.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "8.5.0",
@@ -95,15 +89,12 @@
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0",
- "husky": "9.1.7",
- "lint-staged": "16.3.2",
"prettier": "2.8.1",
"typescript": "4.9.4",
"vite": "5.4.19",
"vite-plugin-pwa": "0.20.5",
"vite-plugin-static-copy": "1.0.4",
- "vite-plugin-top-level-await": "1.4.4",
- "vitest": "1.6.0"
+ "vite-plugin-top-level-await": "1.4.4"
},
"engines": {
"node": ">=16.0.0"
@@ -1658,131 +1649,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@commitlint/config-validator": {
- "version": "20.5.0",
- "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.5.0.tgz",
- "integrity": "sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@commitlint/types": "^20.5.0",
- "ajv": "^8.11.0"
- },
- "engines": {
- "node": ">=v18"
- }
- },
- "node_modules/@commitlint/config-validator/node_modules/ajv": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
- "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/@commitlint/config-validator/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
- "node_modules/@commitlint/execute-rule": {
- "version": "20.0.0",
- "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz",
- "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=v18"
- }
- },
- "node_modules/@commitlint/load": {
- "version": "20.5.0",
- "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.0.tgz",
- "integrity": "sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@commitlint/config-validator": "^20.5.0",
- "@commitlint/execute-rule": "^20.0.0",
- "@commitlint/resolve-extends": "^20.5.0",
- "@commitlint/types": "^20.5.0",
- "cosmiconfig": "^9.0.1",
- "cosmiconfig-typescript-loader": "^6.1.0",
- "is-plain-obj": "^4.1.0",
- "lodash.mergewith": "^4.6.2",
- "picocolors": "^1.1.1"
- },
- "engines": {
- "node": ">=v18"
- }
- },
- "node_modules/@commitlint/resolve-extends": {
- "version": "20.5.0",
- "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.0.tgz",
- "integrity": "sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@commitlint/config-validator": "^20.5.0",
- "@commitlint/types": "^20.5.0",
- "global-directory": "^4.0.1",
- "import-meta-resolve": "^4.0.0",
- "lodash.mergewith": "^4.6.2",
- "resolve-from": "^5.0.0"
- },
- "engines": {
- "node": ">=v18"
- }
- },
- "node_modules/@commitlint/resolve-extends/node_modules/resolve-from": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
- "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@commitlint/types": {
- "version": "20.5.0",
- "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.5.0.tgz",
- "integrity": "sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "conventional-commits-parser": "^6.3.0",
- "picocolors": "^1.1.1"
- },
- "engines": {
- "node": ">=v18"
- }
- },
- "node_modules/@element-hq/element-call-embedded": {
- "version": "0.20.1",
- "resolved": "https://registry.npmjs.org/@element-hq/element-call-embedded/-/element-call-embedded-0.20.1.tgz",
- "integrity": "sha512-ODg2r7UmR8UjRpapLKbn6v1PS8fu/r58zdbvXMYaAlUEAC2f6L/9Moc9S4noG1+ARgWxY+m2vLmNDK9G9uFZYQ==",
- "dev": true
- },
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
@@ -2192,38 +2058,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/@fontsource/ibm-plex-mono": {
- "version": "5.2.5",
- "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.2.5.tgz",
- "integrity": "sha512-G09N3GfuT9qj3Ax2FDZvKqZttzM3v+cco2l8uXamhKyXLdmlaUDH5o88/C3vtTHj2oT7yRKsvxz9F+BXbWKMYA==",
- "license": "OFL-1.1",
- "funding": {
- "url": "https://github.com/sponsors/ayuhito"
- }
- },
"node_modules/@fontsource/inter": {
"version": "4.5.14",
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.14.tgz",
"integrity": "sha512-JDC9AocdPLuGsASkvWw9hS5gtHE7K9dOwL98XLrk5yjYqxy4uVnScG58NUvFMJDVJRl/7c8Wnap6PEs+7Zvj1Q=="
},
- "node_modules/@fontsource/montserrat": {
- "version": "5.2.5",
- "resolved": "https://registry.npmjs.org/@fontsource/montserrat/-/montserrat-5.2.5.tgz",
- "integrity": "sha512-/OA+bdg1yqUArDUnEeCqDwgTDiIpvED56TxYskje8UKpsvjl/0A9eNod7IcdHz6/Osi70vo7briRO3la8/u+5Q==",
- "license": "OFL-1.1",
- "funding": {
- "url": "https://github.com/sponsors/ayuhito"
- }
- },
- "node_modules/@fontsource/ubuntu": {
- "version": "5.2.5",
- "resolved": "https://registry.npmjs.org/@fontsource/ubuntu/-/ubuntu-5.2.5.tgz",
- "integrity": "sha512-VKVFVqmJ9MGnOJW2dsQ982qHN8Zr+tNeEwEHnzt5VdGi9ZWL37wYFD32YsqukWlI/+I7v5ZkgHTEAvsZxU6aDA==",
- "license": "OFL-1.1",
- "funding": {
- "url": "https://github.com/sponsors/ayuhito"
- }
- },
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz",
@@ -2338,19 +2177,6 @@
"@swc/helpers": "^0.5.0"
}
},
- "node_modules/@jest/schemas": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
- "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@sinclair/typebox": "^0.27.8"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
@@ -2391,10 +2217,9 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
- "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
- "license": "MIT"
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
@@ -2431,9 +2256,9 @@
}
},
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.1.tgz",
- "integrity": "sha512-VRjWhE1UgHnPpJ3b9B5+8z71ZC/HICFngPPFIN6ktzmUBKI5RusPujzbAQUoB3CgZ0yU58L99AfSQS4YTztSWw==",
+ "version": "15.3.0",
+ "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-15.3.0.tgz",
+ "integrity": "sha512-QyxHvncvkl7nf+tnn92PjQ54gMNV8hMSpiukiDgNrqF6IYwgySTlcSdkPYdw8QjZJ0NR6fnVrNzMec0OohM3wA==",
"license": "Apache-2.0",
"engines": {
"node": ">= 18"
@@ -3874,10 +3699,9 @@
}
},
"node_modules/@remix-run/router": {
- "version": "1.23.4",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.4.tgz",
- "integrity": "sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q==",
- "license": "MIT",
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.0.tgz",
+ "integrity": "sha512-5dMOnVnefRsl4uRnAdoWjtVTdh8e6aZqgM4puy9nmEADH72ck+uXwzpJLEKE9Q6F8ZljNewLgmTfkxUrBdv4WA==",
"engines": {
"node": ">=14.0.0"
}
@@ -4361,27 +4185,6 @@
"win32"
]
},
- "node_modules/@simple-libs/stream-utils": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz",
- "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://ko-fi.com/dangreen"
- }
- },
- "node_modules/@sinclair/typebox": {
- "version": "0.27.12",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz",
- "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
@@ -4834,14 +4637,19 @@
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true
},
+ "node_modules/@types/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
+ "license": "MIT"
+ },
"node_modules/@types/sanitize-html": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.1.tgz",
- "integrity": "sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==",
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz",
+ "integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "htmlparser2": "^10.1"
+ "htmlparser2": "^8.0.0"
}
},
"node_modules/@types/scheduler": {
@@ -5182,143 +4990,6 @@
"vite": "^4.2.0 || ^5.0.0"
}
},
- "node_modules/@vitest/expect": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz",
- "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "1.6.0",
- "@vitest/utils": "1.6.0",
- "chai": "^4.3.10"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz",
- "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/utils": "1.6.0",
- "p-limit": "^5.0.0",
- "pathe": "^1.1.1"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner/node_modules/p-limit": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
- "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "yocto-queue": "^1.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@vitest/runner/node_modules/pathe": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
- "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@vitest/runner/node_modules/yocto-queue": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
- "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@vitest/snapshot": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz",
- "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "magic-string": "^0.30.5",
- "pathe": "^1.1.1",
- "pretty-format": "^29.7.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/snapshot/node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/@vitest/snapshot/node_modules/pathe": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
- "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@vitest/spy": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz",
- "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyspy": "^2.2.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/utils": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz",
- "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "diff-sequences": "^29.6.3",
- "estree-walker": "^3.0.3",
- "loupe": "^2.3.7",
- "pretty-format": "^29.7.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/utils/node_modules/estree-walker": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "^1.0.0"
- }
- },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -5345,19 +5016,6 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
- "node_modules/acorn-walk": {
- "version": "8.3.5",
- "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
- "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "acorn": "^8.11.0"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -5397,35 +5055,6 @@
"integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==",
"license": "Apache-2.0"
},
- "node_modules/ansi-escapes": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
- "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "type-fest": "^0.21.3"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ansi-escapes/node_modules/type-fest": {
- "version": "0.21.3",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
- "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
- "dev": true,
- "license": "(MIT OR CC0-1.0)",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -5650,16 +5279,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/assertion-error": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
- "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
"node_modules/ast-types-flow": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
@@ -5822,43 +5441,6 @@
"resolved": "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-3.0.0.tgz",
"integrity": "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q=="
},
- "node_modules/bl": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
- },
- "node_modules/bl/node_modules/buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
- }
- },
"node_modules/blurhash": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.4.tgz",
@@ -5969,16 +5551,6 @@
"node": ">=8"
}
},
- "node_modules/cachedir": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
- "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -6069,25 +5641,6 @@
"node": ">=6"
}
},
- "node_modules/chai": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
- "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "assertion-error": "^1.1.0",
- "check-error": "^1.0.3",
- "deep-eql": "^4.1.3",
- "get-func-name": "^2.0.2",
- "loupe": "^2.3.6",
- "pathval": "^1.1.1",
- "type-detect": "^4.1.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -6103,26 +5656,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/chardet": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
- "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/check-error": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
- "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-func-name": "^2.0.2"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -6179,105 +5712,6 @@
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
- "node_modules/cli-cursor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
- "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "restore-cursor": "^3.1.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cli-spinners": {
- "version": "2.9.2",
- "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
- "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-truncate": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz",
- "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "slice-ansi": "^8.0.0",
- "string-width": "^8.2.0"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-truncate/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/cli-truncate/node_modules/string-width": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz",
- "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.5.0",
- "strip-ansi": "^7.1.2"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-truncate/node_modules/strip-ansi": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
- "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.2.2"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/cli-width": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
- "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -6291,16 +5725,6 @@
"node": ">=12"
}
},
- "node_modules/clone": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
- "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8"
- }
- },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -6334,93 +5758,12 @@
"color-support": "bin.js"
}
},
- "node_modules/colorette": {
- "version": "2.0.20",
- "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
- "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
- "node_modules/commitizen": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.3.1.tgz",
- "integrity": "sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cachedir": "2.3.0",
- "cz-conventional-changelog": "3.3.0",
- "dedent": "0.7.0",
- "detect-indent": "6.1.0",
- "find-node-modules": "^2.1.2",
- "find-root": "1.1.0",
- "fs-extra": "9.1.0",
- "glob": "7.2.3",
- "inquirer": "8.2.5",
- "is-utf8": "^0.2.1",
- "lodash": "4.17.21",
- "minimist": "1.2.7",
- "strip-bom": "4.0.0",
- "strip-json-comments": "3.1.1"
- },
- "bin": {
- "commitizen": "bin/commitizen",
- "cz": "bin/git-cz",
- "git-cz": "bin/git-cz"
- },
- "engines": {
- "node": ">= 12"
- }
- },
- "node_modules/commitizen/node_modules/dedent": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
- "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/commitizen/node_modules/fs-extra": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
- "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "at-least-node": "^1.0.0",
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/commitizen/node_modules/minimist": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
- "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/commitizen/node_modules/strip-bom": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
- "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/common-tags": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
@@ -6460,41 +5803,12 @@
"optional": true
},
"node_modules/content-type": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
- "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/conventional-commit-types": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-3.0.0.tgz",
- "integrity": "sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/conventional-commits-parser": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.3.0.tgz",
- "integrity": "sha512-RfOq/Cqy9xV9bOA8N+ZH6DlrDR+5S3Mi0B5kACEjESpE+AviIpAptx9a9cFpWCCvgRtWT+0BbUw+e1BZfts9jg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@simple-libs/stream-utils": "^1.2.0",
- "meow": "^13.0.0"
- },
- "bin": {
- "conventional-commits-parser": "dist/cli/index.js"
- },
- "engines": {
- "node": ">=18"
+ "node": ">= 0.6"
}
},
"node_modules/convert-source-map": {
@@ -6526,73 +5840,6 @@
"url": "https://opencollective.com/core-js"
}
},
- "node_modules/cosmiconfig": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz",
- "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "env-paths": "^2.2.1",
- "import-fresh": "^3.3.0",
- "js-yaml": "^4.1.0",
- "parse-json": "^5.2.0"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/d-fischer"
- },
- "peerDependencies": {
- "typescript": ">=4.9.5"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/cosmiconfig-typescript-loader": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz",
- "integrity": "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "jiti": "^2.6.1"
- },
- "engines": {
- "node": ">=v18"
- },
- "peerDependencies": {
- "@types/node": "*",
- "cosmiconfig": ">=9",
- "typescript": ">=5"
- }
- },
- "node_modules/cosmiconfig/node_modules/parse-json": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
- "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@babel/code-frame": "^7.0.0",
- "error-ex": "^1.3.1",
- "json-parse-even-better-errors": "^2.3.0",
- "lines-and-columns": "^1.1.6"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -6643,105 +5890,6 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
- "node_modules/cz-conventional-changelog": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz",
- "integrity": "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^2.4.1",
- "commitizen": "^4.0.3",
- "conventional-commit-types": "^3.0.0",
- "lodash.map": "^4.5.1",
- "longest": "^2.0.1",
- "word-wrap": "^1.0.3"
- },
- "engines": {
- "node": ">= 10"
- },
- "optionalDependencies": {
- "@commitlint/load": ">6.1.1"
- }
- },
- "node_modules/cz-conventional-changelog/node_modules/ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^1.9.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/cz-conventional-changelog/node_modules/chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/cz-conventional-changelog/node_modules/color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/cz-conventional-changelog/node_modules/color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cz-conventional-changelog/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/cz-conventional-changelog/node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/cz-conventional-changelog/node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -6858,19 +6006,6 @@
}
}
},
- "node_modules/deep-eql": {
- "version": "4.1.4",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
- "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "type-detect": "^4.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -6890,19 +6025,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/defaults": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
- "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "clone": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -6943,26 +6065,6 @@
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"optional": true
},
- "node_modules/detect-file": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
- "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/detect-indent": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
- "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
@@ -6972,16 +6074,6 @@
"node": ">=8"
}
},
- "node_modules/diff-sequences": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
- "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -7141,41 +6233,6 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/env-paths": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
- "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/environment": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
- "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/error-ex": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
- "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "is-arrayish": "^0.2.1"
- }
- },
"node_modules/es-abstract": {
"version": "1.23.9",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
@@ -7889,13 +6946,6 @@
"node": ">= 0.8"
}
},
- "node_modules/eventemitter3": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
- "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -7905,113 +6955,6 @@
"node": ">=0.8.x"
}
},
- "node_modules/execa": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
- "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cross-spawn": "^7.0.3",
- "get-stream": "^8.0.1",
- "human-signals": "^5.0.0",
- "is-stream": "^3.0.0",
- "merge-stream": "^2.0.0",
- "npm-run-path": "^5.1.0",
- "onetime": "^6.0.0",
- "signal-exit": "^4.1.0",
- "strip-final-newline": "^3.0.0"
- },
- "engines": {
- "node": ">=16.17"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/execa?sponsor=1"
- }
- },
- "node_modules/execa/node_modules/is-stream": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
- "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/execa/node_modules/mimic-fn": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
- "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/execa/node_modules/onetime": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
- "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mimic-fn": "^4.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/execa/node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/expand-tilde": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
- "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "homedir-polyfill": "^1.0.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/external-editor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
- "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chardet": "^0.7.0",
- "iconv-lite": "^0.4.24",
- "tmp": "^0.0.33"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -8084,14 +7027,10 @@
}
},
"node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
+ "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
"dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
"peerDependencies": {
"picomatch": "^3 || ^4"
},
@@ -8160,24 +7099,6 @@
"node": ">=8"
}
},
- "node_modules/find-node-modules": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.1.3.tgz",
- "integrity": "sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "findup-sync": "^4.0.0",
- "merge": "^2.1.1"
- }
- },
- "node_modules/find-root": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
- "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -8193,22 +7114,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/findup-sync": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz",
- "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "detect-file": "^1.0.0",
- "is-glob": "^4.0.0",
- "micromatch": "^4.0.2",
- "resolve-dir": "^1.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
"node_modules/flat-cache": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
@@ -8252,9 +7157,9 @@
}
},
"node_modules/folds": {
- "version": "2.7.2",
- "resolved": "https://registry.npmjs.org/folds/-/folds-2.7.2.tgz",
- "integrity": "sha512-T/PQuN+ku/55hMVabamwsGp1uPSKkFw4xPEf7uZjAZNoejUL1s0leecA00sudYM2HvZaIUzUrosp+f0+8rWC6w==",
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/folds/-/folds-2.5.0.tgz",
+ "integrity": "sha512-UJhvXAQ1XnZ9w10KJwSW+frvzzWE/zcF0dH3fDVCD70RFHAxwEi0UkkVS8CaZGxZF2Wvt3qTJyTS5LW3LwwUAw==",
"license": "Apache-2.0",
"peerDependencies": {
"@vanilla-extract/css": "1.9.2",
@@ -8411,29 +7316,6 @@
"node": "6.* || 8.* || >= 10.*"
}
},
- "node_modules/get-east-asian-width": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
- "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/get-func-name": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
- "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
"node_modules/get-intrinsic": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
@@ -8477,19 +7359,6 @@
"node": ">= 0.4"
}
},
- "node_modules/get-stream": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
- "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/get-symbol-description": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
@@ -8540,75 +7409,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/global-directory": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz",
- "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "ini": "4.1.1"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/global-modules": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
- "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "global-prefix": "^1.0.1",
- "is-windows": "^1.0.1",
- "resolve-dir": "^1.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/global-prefix": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
- "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "expand-tilde": "^2.0.2",
- "homedir-polyfill": "^1.0.1",
- "ini": "^1.3.4",
- "is-windows": "^1.0.1",
- "which": "^1.2.14"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/global-prefix/node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/global-prefix/node_modules/which": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
- "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "which": "bin/which"
- }
- },
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -8786,19 +7586,6 @@
"react-is": "^16.7.0"
}
},
- "node_modules/homedir-polyfill": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
- "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "parse-passwd": "^1.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/html-dom-parser": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-4.0.0.tgz",
@@ -8849,10 +7636,9 @@
}
},
"node_modules/htmlparser2": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
- "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
- "dev": true,
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
@@ -8860,25 +7646,11 @@
"url": "https://github.com/sponsors/fb55"
}
],
- "license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
- "domutils": "^3.2.2",
- "entities": "^7.0.1"
- }
- },
- "node_modules/htmlparser2/node_modules/entities": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
- "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
+ "domutils": "^3.0.1",
+ "entities": "^4.4.0"
}
},
"node_modules/https-proxy-agent": {
@@ -8894,32 +7666,6 @@
"node": ">= 6"
}
},
- "node_modules/human-signals": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
- "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=16.17.0"
- }
- },
- "node_modules/husky": {
- "version": "9.1.7",
- "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
- "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "husky": "bin.js"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/typicode"
- }
- },
"node_modules/i18next": {
"version": "23.12.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.12.2.tgz",
@@ -8951,24 +7697,19 @@
}
},
"node_modules/i18next-http-backend": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.3.tgz",
- "integrity": "sha512-B5GJysCsJwqiDQRpUcikWsut82Vmekix58WIyiCpYtxaZ/wY8kwJCicMRvVcMoIa1x84siiRl5vBt6iCBVO6zg==",
- "engines": {
- "node": ">=18"
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.5.2.tgz",
+ "integrity": "sha512-+K8HbDfrvc1/2X8jpb7RLhI9ZxBDpx3xogYkQwGKlWAUXLSEGXzgdt3EcUjLlBCdMwdQY+K+EUF6oh8oB6rwHw==",
+ "dependencies": {
+ "cross-fetch": "4.0.0"
}
},
- "node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "dev": true,
- "license": "MIT",
+ "node_modules/i18next-http-backend/node_modules/cross-fetch": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
+ "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
"dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
- },
- "engines": {
- "node": ">=0.10.0"
+ "node-fetch": "^2.6.12"
}
},
"node_modules/idb": {
@@ -9031,18 +7772,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/import-meta-resolve": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
- "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -9069,75 +7798,11 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"devOptional": true
},
- "node_modules/ini": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
- "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
- "dev": true,
- "license": "ISC",
- "optional": true,
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
"node_modules/inline-style-parser": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
},
- "node_modules/inquirer": {
- "version": "8.2.5",
- "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz",
- "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-escapes": "^4.2.1",
- "chalk": "^4.1.1",
- "cli-cursor": "^3.1.0",
- "cli-width": "^3.0.0",
- "external-editor": "^3.0.3",
- "figures": "^3.0.0",
- "lodash": "^4.17.21",
- "mute-stream": "0.0.8",
- "ora": "^5.4.1",
- "run-async": "^2.4.0",
- "rxjs": "^7.5.5",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0",
- "through": "^2.3.6",
- "wrap-ansi": "^7.0.0"
- },
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/inquirer/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/inquirer/node_modules/figures": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
- "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "escape-string-regexp": "^1.0.5"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -9180,14 +7845,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-arrayish": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
- "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
"node_modules/is-async-function": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.0.tgz",
@@ -9377,16 +8034,6 @@
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
"license": "MIT"
},
- "node_modules/is-interactive": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
- "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/is-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
@@ -9405,18 +8052,6 @@
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true
},
- "node_modules/is-network-error": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz",
- "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==",
- "license": "MIT",
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -9460,25 +8095,10 @@
"node": ">=8"
}
},
- "node_modules/is-plain-obj": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
- "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/is-plain-object": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.1.0.tgz",
- "integrity": "sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==",
- "license": "MIT",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"engines": {
"node": ">=0.10.0"
}
@@ -9597,13 +8217,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-utf8": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
- "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/is-weakmap": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@@ -9647,16 +8260,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-windows": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
- "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@@ -9692,17 +8295,6 @@
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="
},
- "node_modules/jiti": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
- "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "bin": {
- "jiti": "lib/jiti-cli.mjs"
- }
- },
"node_modules/jotai": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.6.0.tgz",
@@ -9767,14 +8359,6 @@
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true
},
- "node_modules/json-parse-even-better-errors": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
- "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
@@ -9876,21 +8460,6 @@
"node": ">=0.10"
}
},
- "node_modules/launder": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/launder/-/launder-1.7.2.tgz",
- "integrity": "sha512-DLg3HPnHUfBi5/MxMLmJD12dmlMpFEC2HgMW6vkZ/9JR0RU00kXoRGvDxAvFmdO0o610OA77i4FgNTLucmhDVg==",
- "license": "MIT",
- "dependencies": {
- "dayjs": "^1.11.23"
- }
- },
- "node_modules/launder/node_modules/dayjs": {
- "version": "1.11.23",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
- "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
- "license": "MIT"
- },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -9921,199 +8490,19 @@
"node": ">=10"
}
},
- "node_modules/lines-and-columns": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
"node_modules/linkify-react": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.3.2.tgz",
- "integrity": "sha512-mi744h1hf+WDsr+paJgSBBgYNLMWNSHyM9V9LVUo03RidNGdw1VpI7Twnt+K3pEh3nIzB4xiiAgZxpd61ItKpQ==",
- "license": "MIT",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz",
+ "integrity": "sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA==",
"peerDependencies": {
"linkifyjs": "^4.0.0",
"react": ">= 15.0.0"
}
},
"node_modules/linkifyjs": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
- "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
- "license": "MIT"
- },
- "node_modules/lint-staged": {
- "version": "16.3.2",
- "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.3.2.tgz",
- "integrity": "sha512-xKqhC2AeXLwiAHXguxBjuChoTTWFC6Pees0SHPwOpwlvI3BH7ZADFPddAdN3pgo3aiKgPUx/bxE78JfUnxQnlg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "commander": "^14.0.3",
- "listr2": "^9.0.5",
- "micromatch": "^4.0.8",
- "string-argv": "^0.3.2",
- "tinyexec": "^1.0.2",
- "yaml": "^2.8.2"
- },
- "bin": {
- "lint-staged": "bin/lint-staged.js"
- },
- "engines": {
- "node": ">=20.17"
- },
- "funding": {
- "url": "https://opencollective.com/lint-staged"
- }
- },
- "node_modules/lint-staged/node_modules/commander": {
- "version": "14.0.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
- "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/lint-staged/node_modules/yaml": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
- "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/eemeli"
- }
- },
- "node_modules/listr2": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
- "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cli-truncate": "^5.0.0",
- "colorette": "^2.0.20",
- "eventemitter3": "^5.0.1",
- "log-update": "^6.1.0",
- "rfdc": "^1.4.1",
- "wrap-ansi": "^9.0.0"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/listr2/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/listr2/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/listr2/node_modules/emoji-regex": {
- "version": "10.6.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
- "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/listr2/node_modules/string-width": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
- "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^10.3.0",
- "get-east-asian-width": "^1.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/listr2/node_modules/strip-ansi": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
- "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.2.2"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/listr2/node_modules/wrap-ansi": {
- "version": "9.0.2",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
- "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.1",
- "string-width": "^7.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/local-pkg": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz",
- "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mlly": "^1.7.3",
- "pkg-types": "^1.2.1"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz",
+ "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg=="
},
"node_modules/locate-path": {
"version": "6.0.0",
@@ -10140,279 +8529,18 @@
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"dev": true
},
- "node_modules/lodash.map": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz",
- "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
- "node_modules/lodash.mergewith": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
- "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
"node_modules/lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
"integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
"dev": true
},
- "node_modules/log-symbols": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
- "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.1.0",
- "is-unicode-supported": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/log-symbols/node_modules/is-unicode-supported": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
- "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/log-update": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
- "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-escapes": "^7.0.0",
- "cli-cursor": "^5.0.0",
- "slice-ansi": "^7.1.0",
- "strip-ansi": "^7.1.0",
- "wrap-ansi": "^9.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/log-update/node_modules/ansi-escapes": {
- "version": "7.3.0",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
- "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "environment": "^1.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/log-update/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/log-update/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/log-update/node_modules/cli-cursor": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
- "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "restore-cursor": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/log-update/node_modules/emoji-regex": {
- "version": "10.6.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
- "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/log-update/node_modules/is-fullwidth-code-point": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
- "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.3.1"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/log-update/node_modules/onetime": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
- "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mimic-function": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/log-update/node_modules/restore-cursor": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
- "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "onetime": "^7.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/log-update/node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/log-update/node_modules/slice-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
- "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.1",
- "is-fullwidth-code-point": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/chalk/slice-ansi?sponsor=1"
- }
- },
- "node_modules/log-update/node_modules/string-width": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
- "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^10.3.0",
- "get-east-asian-width": "^1.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/log-update/node_modules/strip-ansi": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
- "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.2.2"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/log-update/node_modules/wrap-ansi": {
- "version": "9.0.2",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
- "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.1",
- "string-width": "^7.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
"node_modules/loglevel": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
@@ -10426,16 +8554,6 @@
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
- "node_modules/longest": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz",
- "integrity": "sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -10447,16 +8565,6 @@
"loose-envify": "cli.js"
}
},
- "node_modules/loupe": {
- "version": "2.3.7",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
- "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-func-name": "^2.0.1"
- }
- },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -10517,33 +8625,47 @@
"license": "Apache-2.0"
},
"node_modules/matrix-js-sdk": {
- "version": "41.7.0",
- "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-41.7.0.tgz",
- "integrity": "sha512-MP0xNv/VVRbshq00TE6EVo77IIXsQk0KjiVtgKV0t9j/V77a6Klt00QrrO0XykkTUsNC0+mQeBMxnx75rZO86Q==",
+ "version": "38.2.0",
+ "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-38.2.0.tgz",
+ "integrity": "sha512-R3jzK8rDGi/3OXOax8jFFyxblCG9KTT5yuXAbvnZCGcpTm8lZ4mHQAn5UydVD8qiyUMNMpaaMd6/k7N+5I/yaQ==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.12.5",
- "@matrix-org/matrix-sdk-crypto-wasm": "^18.3.1",
+ "@matrix-org/matrix-sdk-crypto-wasm": "^15.1.0",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
- "content-type": "^2.0.0",
+ "content-type": "^1.0.4",
"jwt-decode": "^4.0.0",
"loglevel": "^1.9.2",
"matrix-events-sdk": "0.0.1",
- "matrix-widget-api": "^1.16.1",
+ "matrix-widget-api": "^1.10.0",
"oidc-client-ts": "^3.0.1",
- "p-retry": "8",
- "sdp-transform": "^3.0.0",
- "unhomoglyph": "^1.0.6"
+ "p-retry": "4",
+ "sdp-transform": "^2.14.1",
+ "unhomoglyph": "^1.0.6",
+ "uuid": "11"
},
"engines": {
"node": ">=22.0.0"
}
},
+ "node_modules/matrix-js-sdk/node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
"node_modules/matrix-widget-api": {
- "version": "1.16.1",
- "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.16.1.tgz",
- "integrity": "sha512-oCfTV4xNPo02qIgveqdkIyKQjOPpsjhF3bmJBotHrhr8TsrhVa7kx8PtuiUPnQTjz0tdBle7falR2Fw8VKsedw==",
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.13.1.tgz",
+ "integrity": "sha512-mkOHUVzaN018TCbObfGOSaMW2GoUxOfcxNNlTVx5/HeMk3OSQPQM0C9oEME5Liiv/dBUoSrEB64V8wF7e/gb1w==",
"license": "Apache-2.0",
"dependencies": {
"@types/events": "^3.0.0",
@@ -10558,34 +8680,6 @@
"@babel/runtime": "^7.12.5"
}
},
- "node_modules/meow": {
- "version": "13.2.0",
- "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
- "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/merge": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz",
- "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/merge-stream": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
- "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -10631,29 +8725,6 @@
"millify": "bin/millify"
}
},
- "node_modules/mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/mimic-function": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
- "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/mimic-response": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
@@ -10760,13 +8831,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
- "node_modules/mute-stream": {
- "version": "0.0.8",
- "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
- "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/nan": {
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
@@ -10774,16 +8838,15 @@
"optional": true
},
"node_modules/nanoid": {
- "version": "3.3.20",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
- "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
- "license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -10807,7 +8870,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
- "optional": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -10852,35 +8914,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/npm-run-path": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
- "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/npm-run-path/node_modules/path-key": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
- "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
@@ -11045,22 +9078,6 @@
"wrappy": "1"
}
},
- "node_modules/onetime": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
- "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mimic-fn": "^2.1.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -11078,53 +9095,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/ora": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
- "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "bl": "^4.1.0",
- "chalk": "^4.1.0",
- "cli-cursor": "^3.1.0",
- "cli-spinners": "^2.5.0",
- "is-interactive": "^1.0.0",
- "is-unicode-supported": "^0.1.0",
- "log-symbols": "^4.1.0",
- "strip-ansi": "^6.0.0",
- "wcwidth": "^1.0.1"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ora/node_modules/is-unicode-supported": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
- "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/os-tmpdir": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
- "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/outdent": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz",
@@ -11176,18 +9146,16 @@
}
},
"node_modules/p-retry": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.0.tgz",
- "integrity": "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==",
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
+ "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
"license": "MIT",
"dependencies": {
- "is-network-error": "^1.3.0"
+ "@types/retry": "0.12.0",
+ "retry": "^0.13.1"
},
"engines": {
- "node": ">=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=8"
}
},
"node_modules/parent-module": {
@@ -11202,16 +9170,6 @@
"node": ">=6"
}
},
- "node_modules/parse-passwd": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
- "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/parse-srcset": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
@@ -11272,16 +9230,6 @@
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz",
"integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w=="
},
- "node_modules/pathval": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
- "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
"node_modules/pdfjs-dist": {
"version": "4.2.67",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.2.67.tgz",
@@ -11300,11 +9248,10 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=12"
},
@@ -11332,9 +9279,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.31",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz",
- "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==",
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
+ "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"funding": [
{
"type": "opencollective",
@@ -11349,11 +9296,10 @@
"url": "https://github.com/sponsors/ai"
}
],
- "license": "MIT",
"dependencies": {
- "nanoid": "^3.3.20",
+ "nanoid": "^3.3.8",
"picocolors": "^1.1.1",
- "source-map-js": "^1.2.2"
+ "source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -11423,41 +9369,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/pretty-format": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
- "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "ansi-styles": "^5.0.0",
- "react-is": "^18.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/pretty-format/node_modules/ansi-styles": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
- "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/pretty-format/node_modules/react-is": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
- "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
@@ -11694,12 +9605,11 @@
}
},
"node_modules/react-router": {
- "version": "6.30.6",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.6.tgz",
- "integrity": "sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg==",
- "license": "MIT",
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.20.0.tgz",
+ "integrity": "sha512-pVvzsSsgUxxtuNfTHC4IxjATs10UaAtvLGVSA1tbUE4GDaOSU1Esu2xF5nWLz7KPiMuW8BJWuPFdlGYJ7/rW0w==",
"dependencies": {
- "@remix-run/router": "1.23.4"
+ "@remix-run/router": "1.13.0"
},
"engines": {
"node": ">=14.0.0"
@@ -11709,13 +9619,12 @@
}
},
"node_modules/react-router-dom": {
- "version": "6.30.6",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.6.tgz",
- "integrity": "sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ==",
- "license": "MIT",
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.20.0.tgz",
+ "integrity": "sha512-CbcKjEyiSVpA6UtCHOIYLUYn/UJfwzp55va4yEfpk7JBN3GPqWfHrdLkAvNCcpXr8QoihcDMuk0dzWZxtlB/mQ==",
"dependencies": {
- "@remix-run/router": "1.23.4",
- "react-router": "6.30.6"
+ "@remix-run/router": "1.13.0",
+ "react-router": "6.20.0"
},
"engines": {
"node": ">=14.0.0"
@@ -11729,7 +9638,7 @@
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "devOptional": true,
+ "optional": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -11941,20 +9850,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/resolve-dir": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
- "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "expand-tilde": "^2.0.0",
- "global-modules": "^1.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -11964,18 +9859,13 @@
"node": ">=4"
}
},
- "node_modules/restore-cursor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
- "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
- "dev": true,
+ "node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
"license": "MIT",
- "dependencies": {
- "onetime": "^5.1.0",
- "signal-exit": "^3.0.2"
- },
"engines": {
- "node": ">=8"
+ "node": ">= 4"
}
},
"node_modules/reusify": {
@@ -11988,13 +9878,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/rfdc": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
- "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -12048,16 +9931,6 @@
"fsevents": "~2.3.2"
}
},
- "node_modules/run-async": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
- "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -12081,16 +9954,6 @@
"queue-microtask": "^1.2.2"
}
},
- "node_modules/rxjs": {
- "version": "7.8.2",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
- "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.1.0"
- }
- },
"node_modules/safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -12163,129 +10026,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/sanitize-html": {
- "version": "2.18.1",
- "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.18.1.tgz",
- "integrity": "sha512-2X5lC8fYla8YaT8g73Nj+HcLWXan5dLyCWLgVvaOmfw7JI78IRsyNtIDytTHdn7hBQbIkCah7Zbmd5jabzeDig==",
- "license": "MIT",
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz",
+ "integrity": "sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==",
"dependencies": {
+ "deepmerge": "^4.2.2",
"escape-string-regexp": "^4.0.0",
- "htmlparser2": "^12.0.0",
- "is-plain-object": "^5.1.0",
- "launder": "^1.7.2",
+ "htmlparser2": "^8.0.0",
+ "is-plain-object": "^5.0.0",
"parse-srcset": "^1.0.2",
- "postcss": "^8.5.27"
- },
- "engines": {
- "node": ">=22.12.0"
- }
- },
- "node_modules/sanitize-html/node_modules/dom-serializer": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",
- "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",
- "license": "MIT",
- "dependencies": {
- "domelementtype": "^3.0.0",
- "domhandler": "^6.0.0",
- "entities": "^8.0.0"
- },
- "engines": {
- "node": ">=20.19.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
- }
- },
- "node_modules/sanitize-html/node_modules/domelementtype": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",
- "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fb55"
- }
- ],
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=20.19.0"
- }
- },
- "node_modules/sanitize-html/node_modules/domhandler": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",
- "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",
- "license": "BSD-2-Clause",
- "dependencies": {
- "domelementtype": "^3.0.0"
- },
- "engines": {
- "node": ">=20.19.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/fb55/domhandler?sponsor=1"
- }
- },
- "node_modules/sanitize-html/node_modules/domutils": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",
- "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",
- "license": "BSD-2-Clause",
- "dependencies": {
- "dom-serializer": "^3.0.0",
- "domelementtype": "^3.0.0",
- "domhandler": "^6.0.0"
- },
- "engines": {
- "node": ">=20.19.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/fb55/domutils?sponsor=1"
- }
- },
- "node_modules/sanitize-html/node_modules/entities": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
- "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=20.19.0"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/sanitize-html/node_modules/htmlparser2": {
- "version": "12.0.0",
- "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz",
- "integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==",
- "funding": [
- "https://github.com/fb55/htmlparser2?sponsor=1",
- {
- "type": "github",
- "url": "https://github.com/sponsors/fb55"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "domelementtype": "^3.0.0",
- "domhandler": "^6.0.0",
- "domutils": "^4.0.2",
- "entities": "^8.0.0"
- },
- "engines": {
- "node": ">=20.19.0"
+ "postcss": "^8.3.11"
}
},
"node_modules/scheduler": {
@@ -12306,9 +10057,9 @@
}
},
"node_modules/sdp-transform": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-3.0.0.tgz",
- "integrity": "sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==",
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz",
+ "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==",
"license": "MIT",
"bin": {
"sdp-verify": "checker.js"
@@ -12480,18 +10231,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/siginfo": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
- "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
- "devOptional": true
+ "optional": true
},
"node_modules/simple-concat": {
"version": "1.0.1",
@@ -12534,15 +10278,20 @@
}
},
"node_modules/slate": {
- "version": "0.123.0",
- "resolved": "https://registry.npmjs.org/slate/-/slate-0.123.0.tgz",
- "integrity": "sha512-Oon3HR/QzJQBjuOUJT1jGGlp8Ff7t3Bkr/rJ2lDqxNT4H+cBnXpEVQ/si6hn1ZCHhD2xY/2N91PQoH/rD7kxTg==",
- "license": "MIT"
+ "version": "0.112.0",
+ "resolved": "https://registry.npmjs.org/slate/-/slate-0.112.0.tgz",
+ "integrity": "sha512-PRnfFgDA3tSop4OH47zu4M1R4Uuhm/AmASu29Qp7sGghVFb713kPBKEnSf1op7Lx/nCHkRlCa3ThfHtCBy+5Yw==",
+ "license": "MIT",
+ "dependencies": {
+ "immer": "^10.0.3",
+ "is-plain-object": "^5.0.0",
+ "tiny-warning": "^1.0.3"
+ }
},
"node_modules/slate-dom": {
- "version": "0.123.0",
- "resolved": "https://registry.npmjs.org/slate-dom/-/slate-dom-0.123.0.tgz",
- "integrity": "sha512-OUinp4tvSrAlt64JL9y20Xin08jgnnj1gJmIuPdGvU5MELKXRNZh17a7EKKNOS6OZPAE8Dk9NI1MAIS/Qz0YBw==",
+ "version": "0.112.2",
+ "resolved": "https://registry.npmjs.org/slate-dom/-/slate-dom-0.112.2.tgz",
+ "integrity": "sha512-cozITMlpcBxrov854reM6+TooiHiqpfM/nZPrnjpN1wSiDsAQmYbWUyftC+jlwcpFj80vywfDHzlG6hXIc5h6A==",
"license": "MIT",
"dependencies": {
"@juggle/resize-observer": "^3.4.0",
@@ -12554,13 +10303,13 @@
"tiny-invariant": "1.3.1"
},
"peerDependencies": {
- "slate": ">=0.121.0"
+ "slate": ">=0.99.0"
}
},
"node_modules/slate-history": {
- "version": "0.113.1",
- "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.113.1.tgz",
- "integrity": "sha512-J9NSJ+UG2GxoW0lw5mloaKcN0JI0x2IA5M5FxyGiInpn+QEutxT1WK7S/JneZCMFJBoHs1uu7S7e6pxQjubHmQ==",
+ "version": "0.110.3",
+ "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.110.3.tgz",
+ "integrity": "sha512-sgdff4Usdflmw5ZUbhDkxFwCBQ2qlDKMMkF93w66KdV48vHOgN2BmLrf+2H8SdX8PYIpP/cTB0w8qWC2GwhDVA==",
"license": "MIT",
"dependencies": {
"is-plain-object": "^5.0.0"
@@ -12570,14 +10319,15 @@
}
},
"node_modules/slate-react": {
- "version": "0.123.0",
- "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.123.0.tgz",
- "integrity": "sha512-nQwXL1FEacrY9ZFmatRhoBnsySNUX2x6qB77V3oNHd7wWxBJWuzz4GMrBXcVoRE8Gac7Angf8xaNGzb6zcPlHg==",
+ "version": "0.112.1",
+ "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.112.1.tgz",
+ "integrity": "sha512-V9b+waxPweXqAkSQmKQ1afG4Me6nVQACPpxQtHPIX02N7MXa5f5WilYv+bKt7vKKw+IZC2F0Gjzhv5BekVgP/A==",
"license": "MIT",
"dependencies": {
"@juggle/resize-observer": "^3.4.0",
"direction": "^1.0.4",
"is-hotkey": "^0.2.0",
+ "is-plain-object": "^5.0.0",
"lodash": "^4.17.21",
"scroll-into-view-if-needed": "^3.1.0",
"tiny-invariant": "1.3.1"
@@ -12585,54 +10335,18 @@
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0",
- "slate": ">=0.121.0",
- "slate-dom": ">=0.119.1"
+ "slate": ">=0.99.0",
+ "slate-dom": ">=0.110.2"
}
},
- "node_modules/slice-ansi": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
- "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==",
- "dev": true,
+ "node_modules/slate/node_modules/immer": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
+ "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.3",
- "is-fullwidth-code-point": "^5.1.0"
- },
- "engines": {
- "node": ">=20"
- },
"funding": {
- "url": "https://github.com/chalk/slice-ansi?sponsor=1"
- }
- },
- "node_modules/slice-ansi/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
- "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.3.1"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
}
},
"node_modules/smob": {
@@ -12654,10 +10368,9 @@
}
},
"node_modules/source-map-js": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
- "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
- "license": "BSD-3-Clause",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"engines": {
"node": ">=0.10.0"
}
@@ -12714,39 +10427,15 @@
"deprecated": "Please use @jridgewell/sourcemap-codec instead",
"dev": true
},
- "node_modules/stackback": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
- "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/std-env": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
- "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "devOptional": true,
+ "optional": true,
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
- "node_modules/string-argv": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
- "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.6.19"
- }
- },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -12891,19 +10580,6 @@
"node": ">=10"
}
},
- "node_modules/strip-final-newline": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
- "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -12916,26 +10592,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/strip-literal": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz",
- "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^9.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/strip-literal/node_modules/js-tokens": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
- "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/style-to-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.3.tgz",
@@ -13054,84 +10710,28 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
- "node_modules/through": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
- "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/tiny-invariant": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==",
"license": "MIT"
},
- "node_modules/tinybench": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
- "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyexec": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz",
- "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
+ "node_modules/tiny-warning": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"node_modules/tinyglobby": {
- "version": "0.2.15",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz",
+ "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.3"
+ "fdir": "^6.4.2",
+ "picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tinypool": {
- "version": "0.8.4",
- "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz",
- "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tinyspy": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
- "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tmp": {
- "version": "0.0.33",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
- "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "os-tmpdir": "~1.0.2"
- },
- "engines": {
- "node": ">=0.6.0"
}
},
"node_modules/to-regex-range": {
@@ -13149,8 +10749,7 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "optional": true
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
@@ -13214,16 +10813,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/type-detect": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
- "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/type-fest": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
@@ -13315,7 +10904,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
"dev": true,
- "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -13484,7 +11072,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "devOptional": true
+ "optional": true
},
"node_modules/uuid": {
"version": "10.0.0",
@@ -14052,89 +11640,6 @@
"@esbuild/win32-x64": "0.21.5"
}
},
- "node_modules/vitest": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz",
- "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/expect": "1.6.0",
- "@vitest/runner": "1.6.0",
- "@vitest/snapshot": "1.6.0",
- "@vitest/spy": "1.6.0",
- "@vitest/utils": "1.6.0",
- "acorn-walk": "^8.3.2",
- "chai": "^4.3.10",
- "debug": "^4.3.4",
- "execa": "^8.0.1",
- "local-pkg": "^0.5.0",
- "magic-string": "^0.30.5",
- "pathe": "^1.1.1",
- "picocolors": "^1.0.0",
- "std-env": "^3.5.0",
- "strip-literal": "^2.0.0",
- "tinybench": "^2.5.1",
- "tinypool": "^0.8.3",
- "vite": "^5.0.0",
- "vite-node": "1.6.0",
- "why-is-node-running": "^2.2.2"
- },
- "bin": {
- "vitest": "vitest.mjs"
- },
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@edge-runtime/vm": "*",
- "@types/node": "^18.0.0 || >=20.0.0",
- "@vitest/browser": "1.6.0",
- "@vitest/ui": "1.6.0",
- "happy-dom": "*",
- "jsdom": "*"
- },
- "peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
- "optional": true
- }
- }
- },
- "node_modules/vitest/node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/vitest/node_modules/pathe": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
- "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
@@ -14143,27 +11648,15 @@
"node": ">=0.10.0"
}
},
- "node_modules/wcwidth": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
- "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "defaults": "^1.0.3"
- }
- },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "optional": true
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "optional": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@@ -14268,23 +11761,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/why-is-node-running": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
- "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "siginfo": "^2.0.0",
- "stackback": "0.0.2"
- },
- "bin": {
- "why-is-node-running": "cli.js"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
diff --git a/package.json b/package.json
index 298fbf36..5ad4daab 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cinny",
- "version": "4.12.8",
+ "version": "4.10.0",
"description": "Yet another matrix client",
"main": "index.js",
"type": "module",
@@ -10,25 +10,11 @@
"scripts": {
"start": "vite",
"build": "vite build",
- "preview": "vite preview",
- "lint": "npm run check:eslint && npm run check:prettier",
+ "lint": "yarn check:eslint && yarn check:prettier",
"check:eslint": "eslint src/*",
"check:prettier": "prettier --check .",
"fix:prettier": "prettier --write .",
- "typecheck": "tsc --noEmit",
- "prepare": "husky install",
- "commit": "git-cz",
- "bump": "node scripts/update-version.js",
- "test": "vitest run"
- },
- "lint-staged": {
- "*.{ts,tsx,js,jsx}": "eslint",
- "*": "prettier --ignore-unknown --write"
- },
- "config": {
- "commitizen": {
- "path": "./node_modules/cz-conventional-changelog"
- }
+ "typecheck": "tsc --noEmit"
},
"keywords": [],
"author": "Ajay Bura",
@@ -57,19 +43,18 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
- "folds": "2.7.2",
+ "folds": "2.5.0",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
"i18next-browser-languagedetector": "8.0.0",
- "i18next-http-backend": "4.0.3",
+ "i18next-http-backend": "2.5.2",
"immer": "9.0.16",
"is-hotkey": "0.2.0",
"jotai": "2.6.0",
- "linkify-react": "4.3.2",
- "linkifyjs": "4.3.2",
- "matrix-js-sdk": "41.7.0",
- "matrix-widget-api": "1.16.1",
+ "linkify-react": "4.1.3",
+ "linkifyjs": "4.1.3",
+ "matrix-js-sdk": "38.2.0",
"millify": "6.1.0",
"pdfjs-dist": "4.2.67",
"prismjs": "1.30.0",
@@ -82,19 +67,15 @@
"react-google-recaptcha": "2.1.0",
"react-i18next": "15.0.0",
"react-range": "1.8.14",
- "react-router-dom": "6.30.6",
- "sanitize-html": "2.18.1",
- "slate": "0.123.0",
- "slate-dom": "0.123.0",
- "slate-history": "0.113.1",
- "slate-react": "0.123.0",
- "ua-parser-js": "1.0.35",
- "@fontsource/ubuntu": "5.2.5",
- "@fontsource/montserrat": "5.2.5",
- "@fontsource/ibm-plex-mono": "5.2.5"
+ "react-router-dom": "6.20.0",
+ "sanitize-html": "2.12.1",
+ "slate": "0.112.0",
+ "slate-dom": "0.112.2",
+ "slate-history": "0.110.3",
+ "slate-react": "0.112.1",
+ "ua-parser-js": "1.0.35"
},
"devDependencies": {
- "@element-hq/element-call-embedded": "0.20.1",
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@rollup/plugin-inject": "5.0.3",
"@rollup/plugin-wasm": "6.1.1",
@@ -106,13 +87,12 @@
"@types/react": "18.2.39",
"@types/react-dom": "18.2.17",
"@types/react-google-recaptcha": "2.1.8",
- "@types/sanitize-html": "2.16.1",
+ "@types/sanitize-html": "2.9.0",
"@types/ua-parser-js": "0.7.36",
"@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1",
"@vitejs/plugin-react": "4.2.0",
"buffer": "6.0.3",
- "cz-conventional-changelog": "3.3.0",
"eslint": "8.29.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "8.5.0",
@@ -120,14 +100,11 @@
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0",
- "husky": "9.1.7",
- "lint-staged": "16.3.2",
"prettier": "2.8.1",
"typescript": "4.9.4",
"vite": "5.4.19",
"vite-plugin-pwa": "0.20.5",
"vite-plugin-static-copy": "1.0.4",
- "vite-plugin-top-level-await": "1.4.4",
- "vitest": "1.6.0"
+ "vite-plugin-top-level-await": "1.4.4"
}
}
diff --git a/public/sound/call.ogg b/public/sound/call.ogg
deleted file mode 100644
index 173f73d2..00000000
Binary files a/public/sound/call.ogg and /dev/null differ
diff --git a/scripts/update-version.js b/scripts/update-version.js
deleted file mode 100644
index d3ef1bae..00000000
--- a/scripts/update-version.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import fs from "fs";
-import path from "path";
-import { execSync } from "child_process";
-import { fileURLToPath } from "url";
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-
-const version = process.argv[2];
-
-if (!version) {
- console.error("Version argument missing");
- process.exit(1);
-}
-
-const root = path.resolve(__dirname, "..");
-const newVersionTag = `v${version}`;
-
-// Update package.json + package-lock.json safely
-execSync(`npm version ${version} --no-git-tag-version`, {
- cwd: root,
- stdio: "inherit",
-});
-
-console.log(`Updated package.json and package-lock.json β ${version}`);
-
-// Update UI version references
-const files = [
- "src/app/features/settings/about/About.tsx",
- "src/app/pages/auth/AuthFooter.tsx",
- "src/app/pages/client/WelcomePage.tsx",
-];
-
-files.forEach((filePath) => {
- const absPath = path.join(root, filePath);
-
- if (!fs.existsSync(absPath)) {
- console.warn(`File not found: ${filePath}`);
- return;
- }
-
- const content = fs.readFileSync(absPath, "utf8");
- const updated = content.replace(/v\d+\.\d+\.\d+/g, newVersionTag);
-
- fs.writeFileSync(absPath, updated);
-
- console.log(`Updated ${filePath} β ${newVersionTag}`);
-});
\ No newline at end of file
diff --git a/src/app/components/BackRouteHandler.tsx b/src/app/components/BackRouteHandler.tsx
index 3b13e487..fa3d7592 100644
--- a/src/app/components/BackRouteHandler.tsx
+++ b/src/app/components/BackRouteHandler.tsx
@@ -51,12 +51,8 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
},
location.pathname
);
- const encodedSpaceIdOrAlias = spaceMatch?.params.spaceIdOrAlias;
- const decodedSpaceIdOrAlias =
- encodedSpaceIdOrAlias && decodeURIComponent(encodedSpaceIdOrAlias);
-
- if (decodedSpaceIdOrAlias) {
- navigate(getSpacePath(decodedSpaceIdOrAlias));
+ if (spaceMatch?.params.spaceIdOrAlias) {
+ navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias));
return;
}
if (
diff --git a/src/app/components/CallEmbedProvider.tsx b/src/app/components/CallEmbedProvider.tsx
deleted file mode 100644
index a6f6dec5..00000000
--- a/src/app/components/CallEmbedProvider.tsx
+++ /dev/null
@@ -1,405 +0,0 @@
-/* eslint-disable jsx-a11y/media-has-caption */
-import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
-import { useAtomValue, useSetAtom } from 'jotai';
-import FocusTrap from 'focus-trap-react';
-import {
- Avatar,
- Box,
- Button,
- color,
- config,
- Dialog,
- Icon,
- Icons,
- Overlay,
- OverlayBackdrop,
- OverlayCenter,
- Text,
- toRem,
-} from 'folds';
-import {
- EventTimelineSetHandlerMap,
- EventType,
- RelationType,
- Room,
- RoomEvent,
-} from 'matrix-js-sdk';
-import { IRTCNotificationContent, RTCNotificationType } from 'matrix-js-sdk/lib/matrixrtc/types';
-import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
-import {
- CallEmbedContextProvider,
- CallEmbedRefContextProvider,
- useCallHangupEvent,
- useCallJoined,
- useCallThemeSync,
- useCallMemberSoundSync,
- useCallStart,
-} 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';
-import { useMatrixClient } from '../hooks/useMatrixClient';
-import CallSound from '../../../public/sound/call.ogg';
-import { useCallMembersChange, useCallSession } from '../hooks/useCall';
-import { useRoomAvatar, useRoomName } from '../hooks/useRoomMeta';
-import { mDirectAtom } from '../state/mDirectList';
-import { useMediaAuthentication } from '../hooks/useMediaAuthentication';
-import { mxcUrlToHttp } from '../utils/matrix';
-import { RoomAvatar, RoomIcon } from './room-avatar';
-import { useRoomNavigate } from '../hooks/useRoomNavigate';
-import { getStateEvent } from '../utils/room';
-import { StateEvent } from '../../types/matrix/room';
-import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels';
-import { getRoomCreatorsForRoomId } from '../hooks/useRoomCreators';
-import { getRoomPermissionsAPI } from '../hooks/useRoomPermissions';
-import { useLivekitSupport } from '../hooks/useLivekitSupport';
-import { CallAvatarAnimation } from '../styles/Animations.css';
-import { webRTCSupported } from '../utils/rtc';
-
-type IncomingCallInfo = {
- room: Room;
- sender: string;
- senderTs: number;
- lifetime: number;
- intent?: string;
- notificationType: RTCNotificationType;
- refEventId: string;
-};
-type IncomingCallProps = {
- dm: boolean;
- info: IncomingCallInfo;
- onIgnore: () => void;
- onAnswer: (room: Room, video: boolean) => void;
- onReject: (room: Room, eventId: string) => void;
-};
-function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallProps) {
- const mx = useMatrixClient();
- const useAuthentication = useMediaAuthentication();
- const livekitSupported = useLivekitSupport();
- const rtcSupported = webRTCSupported();
- const canAnswer = livekitSupported && rtcSupported;
- const { room } = info;
-
- const audioRef = useRef(null);
-
- const roomName = useRoomName(room);
- const roomAvatar = useRoomAvatar(room, dm);
- const avatarUrl = roomAvatar
- ? mxcUrlToHttp(mx, roomAvatar, useAuthentication, 96, 96, 'crop') ?? undefined
- : undefined;
-
- const session = useCallSession(room);
- useCallMembersChange(
- session,
- useCallback(
- (members) => {
- if (members.length === 0) {
- onIgnore();
- }
- },
- [onIgnore]
- )
- );
-
- const playSound = useCallback(() => {
- const audioElement = audioRef.current;
- audioElement?.play();
- }, []);
-
- useEffect(() => {
- if (info.notificationType === 'ring') {
- playSound();
- }
- }, [playSound, info.notificationType]);
-
- return (
- <>
- }>
-
- onIgnore(),
- clickOutsideDeactivates: false,
- escapeDeactivates: false,
- }}
- >
-
-
-
- {info.sender}
-
-
-
-
- (
-
- )}
- />
-
-
-
-
- {roomName}
-
-
- Incoming Call
-
-
-
- {!livekitSupported && (
-
- Your homeserver does not support calling.
-
- )}
- {!webRTCSupported && (
-
- Your browser does not support WebRTC, which is required for calling.
-
- )}
-
- onAnswer(room, info.intent === 'video')}
- before={
-
- }
- disabled={!canAnswer}
- >
-
- Answer
-
-
- (dm ? onReject(room, info.refEventId) : onIgnore())}
- before={ }
- >
-
- {dm ? 'Reject' : 'Ignore'}
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
-
-type IncomingCallListenerProps = {
- callEmbed?: CallEmbed;
- joined?: boolean;
-};
-function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps) {
- const mx = useMatrixClient();
- const directs = useAtomValue(mDirectAtom);
- const { navigateRoom } = useRoomNavigate();
-
- const [callInfo, setCallInfo] = useState();
- const dm = callInfo ? directs.has(callInfo.room.roomId) : false;
- const startCall = useCallStart(dm);
-
- const handleTimelineEvent: EventTimelineSetHandlerMap[RoomEvent.Timeline] = useCallback(
- async (event, room, toStartOfTimeline, removed, data) => {
- // only process rtc notification reference events.
- // we do not want to wait to decrypt all events.
- if (event.getRelation()?.rel_type !== RelationType.Reference) return;
- if (room?.isCallRoom()) return;
-
- if (event.isEncrypted()) {
- if (!event.isBeingDecrypted()) {
- await event.attemptDecryption(mx.getCrypto() as CryptoBackend);
- }
- await event.getDecryptionPromise();
- }
-
- if (
- !room ||
- event.getType() !== EventType.RTCNotification ||
- event.getSender() === mx.getSafeUserId() ||
- !data.liveEvent
- ) {
- return;
- }
-
- const sender = event.getSender();
- const content = event.getContent();
- const senderTs =
- content.sender_ts - event.getTs() > 20000 ? event.getTs() : content.sender_ts;
- const lifetime = Math.min(content.lifetime, 120000);
- const notificationType = content.notification_type;
- const relation =
- event.getRelation()?.rel_type === RelationType.Reference ? event.getRelation() : undefined;
- const refEventId = relation?.event_id;
-
- const mention =
- content['m.mentions']?.room ||
- content['m.mentions']?.user_ids?.includes(mx.getSafeUserId());
- if (!sender || !refEventId || !mention || Date.now() >= senderTs + lifetime) {
- return;
- }
-
- const powerLevelsEvent = getStateEvent(room, StateEvent.RoomPowerLevels);
- const powerLevels = getPowersLevelFromMatrixEvent(powerLevelsEvent);
- const creators = getRoomCreatorsForRoomId(mx, room.roomId);
- const permissions = getRoomPermissionsAPI(creators, powerLevels);
-
- const hasCallPermission = permissions.stateEvent(
- StateEvent.GroupCallMemberPrefix,
- mx.getSafeUserId()
- );
- if (!hasCallPermission) return;
-
- const info: IncomingCallInfo = {
- room,
- sender,
- senderTs,
- lifetime,
- intent:
- 'm.call.intent' in content && typeof content['m.call.intent'] === 'string'
- ? content['m.call.intent']
- : undefined,
- notificationType,
- refEventId,
- };
-
- setCallInfo(info);
- },
- [mx]
- );
-
- useEffect(() => {
- mx.on(RoomEvent.Timeline, handleTimelineEvent);
- return () => {
- mx.removeListener(RoomEvent.Timeline, handleTimelineEvent);
- };
- }, [mx, handleTimelineEvent]);
-
- const handleIgnore = useCallback(() => {
- setCallInfo(undefined);
- }, []);
-
- const handleReject = useCallback(
- (room: Room, eventId: string) => {
- mx.sendEvent(room.roomId, EventType.RTCDecline, {
- 'm.relates_to': {
- rel_type: RelationType.Reference,
- event_id: eventId,
- },
- });
- setCallInfo(undefined);
- },
- [mx]
- );
-
- const handleAnswer = useCallback(
- (room: Room, video: boolean) => {
- startCall(room, { microphone: true, video, sound: true });
- setCallInfo(undefined);
- navigateRoom(room.roomId);
- },
- [startCall, navigateRoom]
- );
-
- if (callInfo && callEmbed?.roomId === callInfo.room.roomId) {
- return null;
- }
- return !joined && callInfo ? (
-
- ) : null;
-}
-
-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(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 (
-
- {callEmbed && }
-
-
- {children}
-
-
-
- );
-}
diff --git a/src/app/components/ImageOverlay.tsx b/src/app/components/ImageOverlay.tsx
deleted file mode 100644
index ea690924..00000000
--- a/src/app/components/ImageOverlay.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import FocusTrap from 'focus-trap-react';
-import { as, Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
-import React, { ReactNode } from 'react';
-import { ModalWide } from '../styles/Modal.css';
-import { stopPropagation } from '../utils/keyboard';
-
-export type RenderViewerProps = {
- src: string;
- alt: string;
- requestClose: () => void;
-};
-
-type ImageOverlayProps = RenderViewerProps & {
- viewer: boolean;
- renderViewer: (props: RenderViewerProps) => ReactNode;
-};
-
-export const ImageOverlay = as<'div', ImageOverlayProps>(
- ({ src, alt, viewer, requestClose, renderViewer, ...props }, ref) => (
- }>
-
- requestClose(),
- clickOutsideDeactivates: true,
- escapeDeactivates: stopPropagation,
- }}
- >
- evt.stopPropagation()}
- >
- {renderViewer({
- src,
- alt,
- requestClose,
- })}
-
-
-
-
- )
-);
diff --git a/src/app/components/JoinRulesSwitcher.tsx b/src/app/components/JoinRulesSwitcher.tsx
index bbc0a65d..9507317a 100644
--- a/src/app/components/JoinRulesSwitcher.tsx
+++ b/src/app/components/JoinRulesSwitcher.tsx
@@ -16,24 +16,34 @@ import {
import { JoinRule } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react';
import { stopPropagation } from '../utils/keyboard';
-import { getRoomIconSrc } from '../utils/room';
export type ExtraJoinRules = 'knock_restricted';
export type ExtendedJoinRules = JoinRule | ExtraJoinRules;
type JoinRuleIcons = Record;
-
-export const useJoinRuleIcons = (roomType?: string): JoinRuleIcons =>
+export const useRoomJoinRuleIcon = (): JoinRuleIcons =>
useMemo(
() => ({
- [JoinRule.Invite]: getRoomIconSrc(Icons, roomType, JoinRule.Invite),
- [JoinRule.Knock]: getRoomIconSrc(Icons, roomType, JoinRule.Knock),
- knock_restricted: getRoomIconSrc(Icons, roomType, JoinRule.Restricted),
- [JoinRule.Restricted]: getRoomIconSrc(Icons, roomType, JoinRule.Restricted),
- [JoinRule.Public]: getRoomIconSrc(Icons, roomType, JoinRule.Public),
- [JoinRule.Private]: getRoomIconSrc(Icons, roomType, JoinRule.Private),
+ [JoinRule.Invite]: Icons.HashLock,
+ [JoinRule.Knock]: Icons.HashLock,
+ knock_restricted: Icons.Hash,
+ [JoinRule.Restricted]: Icons.Hash,
+ [JoinRule.Public]: Icons.HashGlobe,
+ [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;
diff --git a/src/app/components/create-room/CreateRoomAccessSelector.tsx b/src/app/components/create-room/CreateRoomKindSelector.tsx
similarity index 54%
rename from src/app/components/create-room/CreateRoomAccessSelector.tsx
rename to src/app/components/create-room/CreateRoomKindSelector.tsx
index 35f39af8..096954fb 100644
--- a/src/app/components/create-room/CreateRoomAccessSelector.tsx
+++ b/src/app/components/create-room/CreateRoomKindSelector.tsx
@@ -2,39 +2,43 @@ import React from 'react';
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
import { SequenceCard } from '../sequence-card';
import { SettingTile } from '../setting-tile';
-import { CreateRoomAccess } from './types';
-type CreateRoomAccessSelectorProps = {
- value?: CreateRoomAccess;
- onSelect: (value: CreateRoomAccess) => void;
+export enum CreateRoomKind {
+ Private = 'private',
+ Restricted = 'restricted',
+ Public = 'public',
+}
+type CreateRoomKindSelectorProps = {
+ value?: CreateRoomKind;
+ onSelect: (value: CreateRoomKind) => void;
canRestrict?: boolean;
disabled?: boolean;
- getIcon: (access: CreateRoomAccess) => IconSrc;
+ getIcon: (kind: CreateRoomKind) => IconSrc;
};
-export function CreateRoomAccessSelector({
+export function CreateRoomKindSelector({
value,
onSelect,
canRestrict,
disabled,
getIcon,
-}: CreateRoomAccessSelectorProps) {
+}: CreateRoomKindSelectorProps) {
return (
{canRestrict && (
onSelect(CreateRoomAccess.Restricted)}
+ aria-pressed={value === CreateRoomKind.Restricted}
+ onClick={() => onSelect(CreateRoomKind.Restricted)}
disabled={disabled}
>
}
- after={value === CreateRoomAccess.Restricted && }
+ before={ }
+ after={value === CreateRoomKind.Restricted && }
>
Restricted
@@ -45,18 +49,18 @@ export function CreateRoomAccessSelector({
)}
onSelect(CreateRoomAccess.Private)}
+ aria-pressed={value === CreateRoomKind.Private}
+ onClick={() => onSelect(CreateRoomKind.Private)}
disabled={disabled}
>
}
- after={value === CreateRoomAccess.Private && }
+ before={ }
+ after={value === CreateRoomKind.Private && }
>
Private
@@ -66,18 +70,18 @@ export function CreateRoomAccessSelector({
onSelect(CreateRoomAccess.Public)}
+ aria-pressed={value === CreateRoomKind.Public}
+ onClick={() => onSelect(CreateRoomKind.Public)}
disabled={disabled}
>
}
- after={value === CreateRoomAccess.Public && }
+ before={ }
+ after={value === CreateRoomKind.Public && }
>
Public
diff --git a/src/app/components/create-room/CreateRoomTypeSelector.tsx b/src/app/components/create-room/CreateRoomTypeSelector.tsx
deleted file mode 100644
index b1f69c39..00000000
--- a/src/app/components/create-room/CreateRoomTypeSelector.tsx
+++ /dev/null
@@ -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 (
-
- onSelect(CreateRoomType.TextRoom)}
- disabled={disabled}
- >
- }
- after={value === CreateRoomType.TextRoom && }
- >
-
-
- Chat Room
-
-
- - Messages, photos, and videos.
-
-
-
-
- onSelect(CreateRoomType.VoiceRoom)}
- disabled={disabled}
- >
- }
- after={value === CreateRoomType.VoiceRoom && }
- >
-
-
- Voice Room
-
-
- - Live audio and video conversations.
-
-
-
-
-
-
- );
-}
diff --git a/src/app/components/create-room/index.ts b/src/app/components/create-room/index.ts
index cce6e037..ffd9cb3d 100644
--- a/src/app/components/create-room/index.ts
+++ b/src/app/components/create-room/index.ts
@@ -1,6 +1,5 @@
-export * from './CreateRoomAccessSelector';
+export * from './CreateRoomKindSelector';
export * from './CreateRoomAliasInput';
export * from './RoomVersionSelector';
export * from './utils';
export * from './AdditionalCreatorInput';
-export * from './types';
diff --git a/src/app/components/create-room/types.ts b/src/app/components/create-room/types.ts
deleted file mode 100644
index 8b54587d..00000000
--- a/src/app/components/create-room/types.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export enum CreateRoomType {
- TextRoom = 'text',
- VoiceRoom = 'voice',
-}
-
-export enum CreateRoomAccess {
- Private = 'private',
- Restricted = 'restricted',
- Public = 'public',
-}
diff --git a/src/app/components/create-room/utils.ts b/src/app/components/create-room/utils.ts
index f3e699aa..a0ca7488 100644
--- a/src/app/components/create-room/utils.ts
+++ b/src/app/components/create-room/utils.ts
@@ -7,10 +7,10 @@ import {
Room,
} from 'matrix-js-sdk';
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
+import { CreateRoomKind } from './CreateRoomKindSelector';
import { RoomType, StateEvent } from '../../../types/matrix/room';
import { getViaServers } from '../../plugins/via-servers';
import { getMxIdServer } from '../../utils/matrix';
-import { CreateRoomAccess } from './types';
export const createRoomCreationContent = (
type: RoomType | undefined,
@@ -32,7 +32,7 @@ export const createRoomCreationContent = (
};
export const createRoomJoinRulesState = (
- access: CreateRoomAccess,
+ kind: CreateRoomKind,
parent: Room | undefined,
knock: boolean
) => {
@@ -40,13 +40,13 @@ export const createRoomJoinRulesState = (
join_rule: knock ? JoinRule.Knock : JoinRule.Invite,
};
- if (access === CreateRoomAccess.Public) {
+ if (kind === CreateRoomKind.Public) {
content = {
join_rule: JoinRule.Public,
};
}
- if (access === CreateRoomAccess.Restricted && parent) {
+ if (kind === CreateRoomKind.Restricted && parent) {
content = {
join_rule: knock ? ('knock_restricted' as JoinRule) : JoinRule.Restricted,
allow: [
@@ -74,10 +74,6 @@ export const createRoomParentState = (parent: Room) => ({
},
});
-const createSpacePowerLevelsOverride = () => ({
- events_default: 50,
-});
-
export const createRoomEncryptionState = () => ({
type: 'm.room.encryption',
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 = {
version: string;
type?: RoomType;
parent?: Room;
- access: CreateRoomAccess;
+ kind: CreateRoomKind;
name: string;
topic?: string;
aliasLocalPart?: string;
@@ -122,11 +106,7 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
initialState.push(createRoomParentState(data.parent));
}
- if (data.type === RoomType.Call) {
- initialState.push(createRoomCallState());
- }
-
- initialState.push(createRoomJoinRulesState(data.access, data.parent, data.knock));
+ initialState.push(createRoomJoinRulesState(data.kind, data.parent, data.knock));
const options: ICreateRoomOpts = {
room_version: data.version,
@@ -138,15 +118,9 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
data.allowFederation,
data.additionalCreators
),
- power_level_content_override:
- data.type === RoomType.Call ? createVoiceRoomPowerLevelsOverride() : undefined,
initial_state: initialState,
};
- if (data.type === RoomType.Space) {
- options.power_level_content_override = createSpacePowerLevelsOverride();
- }
-
const result = await mx.createRoom(options);
if (data.parent) {
diff --git a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
index d358ff7d..cc0dff19 100644
--- a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
+++ b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
@@ -88,8 +88,6 @@ export function EmoticonAutocomplete({
{autoCompleteEmoticon.map((emoticon) => {
const isCustomEmoji = 'url' in emoticon;
const key = isCustomEmoji ? emoticon.url : emoticon.unicode;
- const customEmojiUrl = mxcUrlToHttp(mx, key, useAuthentication);
-
return (
handleAutocomplete(key, emoticon.shortcode)}
before={
- isCustomEmoji && customEmojiUrl ? (
+ isCustomEmoji ? (
diff --git a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx
index 377cecab..b0c64f60 100644
--- a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx
+++ b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx
@@ -169,13 +169,12 @@ export function RoomMentionAutocomplete({
)}
/>
) : (
-
+
)}
}
diff --git a/src/app/components/editor/input.ts b/src/app/components/editor/input.ts
index 56da5893..ad314add 100644
--- a/src/app/components/editor/input.ts
+++ b/src/app/components/editor/input.ts
@@ -157,12 +157,10 @@ const getInlineElement = (node: ChildNode, processText: ProcessTextCallback): In
return children;
}
- const children = node.childNodes.flatMap((child) => getInlineElement(child, processText));
- if (children.length === 0) return [{ text: '' }];
- return children;
+ return node.childNodes.flatMap((child) => getInlineElement(child, processText));
}
- return [{ text: '' }];
+ return [];
};
const parseBlockquoteNode = (
@@ -193,7 +191,7 @@ const parseBlockquoteNode = (
if (child.name === 'p') {
appendLine();
- quoteLines.push(getInlineElement(child, processText));
+ quoteLines.push(child.children.flatMap((c) => getInlineElement(c, processText)));
return;
}
@@ -230,13 +228,9 @@ const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElemen
children: [{ text }],
}));
const childCode = node.children[0];
- const attribs =
- isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs : undefined;
- const languageClass = attribs?.class;
- const customLabel = attribs?.['data-label'];
- const prefix = {
- text: `${mdSequence}${customLabel ?? languageClass?.replace('language-', '') ?? ''}`,
- };
+ const className =
+ isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs.class ?? '' : '';
+ const prefix = { text: `${mdSequence}${className.replace('language-', '')}` };
const suffix = { text: mdSequence };
return [
{ type: BlockType.Paragraph, children: [prefix] },
@@ -255,67 +249,10 @@ const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElemen
},
];
};
-
-const parseListMarkdown = (
+const parseListNode = (
node: Element,
- processText: ProcessTextCallback,
- depth = 0
-): ParagraphElement[] => {
- const md = isTag(node) && node.name === 'ul' ? '*' : '-';
- const prefix = node.attribs['data-md'] ?? md;
- const [starOrHyphen] = prefix.match(/^\*|-$/) ?? [];
- const [digitOrChar] = prefix.match(/^[\da-zA-Z]/) ?? [];
-
- const digit = digitOrChar ? parseInt(digitOrChar, 10) : undefined;
-
- const lines: ParagraphElement[] = [];
- let lineNo = digit === undefined || Number.isNaN(digit) ? digitOrChar ?? 1 : digit;
- const pushLine = (line: InlineElement[]) => {
- lines.push({
- type: BlockType.Paragraph,
- children: [
- {
- text: `${Array(depth + 1).join(' ')}${starOrHyphen ? `${starOrHyphen} ` : `${lineNo}. `}`,
- },
- ...line,
- ],
- });
- if (typeof lineNo === 'string') {
- lineNo = String.fromCharCode(lineNo.charCodeAt(0) + 1);
- } else {
- lineNo += 1;
- }
- };
-
- node.children.forEach((child) => {
- if (isText(child)) {
- pushLine([{ text: processText(child.data) }]);
- return;
- }
-
- if (isTag(child)) {
- if (child.name === 'ul' || child.name === 'ol') {
- lines.push(...parseListMarkdown(child, processText, depth + 1));
- return;
- }
- if (child.name === 'li') {
- child.children.forEach((c) => {
- if (isTag(c) && (c.name === 'ul' || c.name === 'ol')) {
- lines.push(...parseListMarkdown(c, processText, depth + 1));
- return;
- }
- pushLine(getInlineElement(c, processText));
- });
- return;
- }
- }
-
- pushLine(getInlineElement(child, processText));
- });
-
- return lines;
-};
-const parseListLines = (children: ChildNode[], processText: ProcessTextCallback) => {
+ processText: ProcessTextCallback
+): OrderedListElement[] | UnorderedListElement[] | ParagraphElement[] => {
const listLines: Array = [];
let lineHolder: InlineElement[] = [];
@@ -326,7 +263,7 @@ const parseListLines = (children: ChildNode[], processText: ProcessTextCallback)
lineHolder = [];
};
- children.forEach((child) => {
+ node.children.forEach((child) => {
if (isText(child)) {
lineHolder.push({ text: processText(child.data) });
return;
@@ -340,7 +277,7 @@ const parseListLines = (children: ChildNode[], processText: ProcessTextCallback)
if (child.name === 'li') {
appendLine();
- listLines.push(getInlineElement(child, processText));
+ listLines.push(child.children.flatMap((c) => getInlineElement(c, processText)));
return;
}
@@ -349,23 +286,24 @@ const parseListLines = (children: ChildNode[], processText: ProcessTextCallback)
});
appendLine();
- return listLines;
-};
-const parseListNode = (
- node: Element,
- processText: ProcessTextCallback
-): OrderedListElement[] | UnorderedListElement[] | ParagraphElement[] => {
- if (node.attribs['data-md'] !== undefined) {
- return parseListMarkdown(node, processText);
+ const mdSequence = node.attribs['data-md'];
+ if (mdSequence !== undefined) {
+ const prefix = mdSequence || '-';
+ const [starOrHyphen] = prefix.match(/^\*|-$/) ?? [];
+ return listLines.map((lineChildren) => ({
+ type: BlockType.Paragraph,
+ children: [
+ { text: `${starOrHyphen ? `${starOrHyphen} ` : `${prefix}. `} ` },
+ ...lineChildren,
+ ],
+ }));
}
- const lines = parseListLines(node.childNodes, processText);
-
if (node.name === 'ol') {
return [
{
type: BlockType.OrderedList,
- children: lines.map((lineChildren) => ({
+ children: listLines.map((lineChildren) => ({
type: BlockType.ListItem,
children: lineChildren,
})),
@@ -376,7 +314,7 @@ const parseListNode = (
return [
{
type: BlockType.UnorderedList,
- children: lines.map((lineChildren) => ({
+ children: listLines.map((lineChildren) => ({
type: BlockType.ListItem,
children: lineChildren,
})),
@@ -387,7 +325,7 @@ const parseHeadingNode = (
node: Element,
processText: ProcessTextCallback
): HeadingElement | ParagraphElement => {
- const children = getInlineElement(node, processText);
+ const children = node.children.flatMap((child) => getInlineElement(child, processText));
const headingMatch = node.name.match(/^h([123456])$/);
const [, g1AsLevel] = headingMatch ?? ['h3', '3'];
@@ -450,7 +388,7 @@ export const domToEditorInput = (
appendLine();
children.push({
type: BlockType.Paragraph,
- children: getInlineElement(node, processText),
+ children: node.children.flatMap((child) => getInlineElement(child, processText)),
});
return;
}
diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts
index 2942a831..dbdd51f3 100644
--- a/src/app/components/editor/output.ts
+++ b/src/app/components/editor/output.ts
@@ -11,7 +11,7 @@ import {
} from '../../plugins/markdown';
import { findAndReplace } from '../../utils/findAndReplace';
import { sanitizeForRegex } from '../../utils/regex';
-import { isUserId } from '../../utils/matrix';
+import { getCanonicalAliasOrRoomId, isUserId } from '../../utils/matrix';
export type OutputOptions = {
allowTextFormatting?: boolean;
@@ -212,10 +212,9 @@ export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): M
if (node.type === BlockType.CodeBlock) return;
if (node.type === BlockType.Mention) {
- if (node.name === '@room') {
+ if (node.id === getCanonicalAliasOrRoomId(mx, roomId)) {
mentionData.room = true;
}
-
if (isUserId(node.id) && node.id !== mx.getUserId()) {
mentionData.users.add(node.id);
}
diff --git a/src/app/components/editor/utils.ts b/src/app/components/editor/utils.ts
index f63c5ac9..90c549c8 100644
--- a/src/app/components/editor/utils.ts
+++ b/src/app/components/editor/utils.ts
@@ -241,15 +241,15 @@ export const getPointUntilChar = (
return targetPoint;
};
-export const getPrevWordRange = (editor: Editor): BaseRange | undefined => {
+export const getPrevWorldRange = (editor: Editor): BaseRange | undefined => {
const { selection } = editor;
if (!selection || !Range.isCollapsed(selection)) return undefined;
const [cursorPoint] = Range.edges(selection);
- const wordStartPoint = getPointUntilChar(editor, cursorPoint, {
+ const worldStartPoint = getPointUntilChar(editor, cursorPoint, {
reverse: true,
- match: (char) => char === ' ' || char === '',
+ match: (char) => char === ' ',
});
- return wordStartPoint && Editor.range(editor, wordStartPoint, cursorPoint);
+ return worldStartPoint && Editor.range(editor, worldStartPoint, cursorPoint);
};
export const isEmptyEditor = (editor: Editor): boolean => {
diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx
index d5a76c71..3db27e2a 100644
--- a/src/app/components/emoji-board/EmojiBoard.tsx
+++ b/src/app/components/emoji-board/EmojiBoard.tsx
@@ -202,7 +202,8 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
const url =
- mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined;
+ mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ||
+ pack.meta.avatar;
return (
);
@@ -98,7 +98,7 @@ export function StickerItem({ mx, useAuthentication, image }: StickerItemProps)
loading="lazy"
className={css.StickerImg}
alt={image.body || image.shortcode}
- src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
+ src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
/>
);
diff --git a/src/app/components/message/FileHeader.tsx b/src/app/components/message/FileHeader.tsx
index 2ffc9ec4..0248862d 100644
--- a/src/app/components/message/FileHeader.tsx
+++ b/src/app/components/message/FileHeader.tsx
@@ -27,8 +27,7 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow
const [downloadState, download] = useAsyncCallback(
useCallback(async () => {
- const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
- if (!mediaUrl) throw new Error('Invalid media URL');
+ const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);
diff --git a/src/app/components/message/MsgTypeRenderers.tsx b/src/app/components/message/MsgTypeRenderers.tsx
index abbf354d..a40ecae1 100644
--- a/src/app/components/message/MsgTypeRenderers.tsx
+++ b/src/app/components/message/MsgTypeRenderers.tsx
@@ -389,8 +389,6 @@ export function MLocation({ content }: MLocationProps) {
const geoUri = content.geo_uri;
if (typeof geoUri !== 'string') return ;
const location = parseGeoUri(geoUri);
- if (!location) return ;
-
return (
{geoUri}
diff --git a/src/app/components/message/Time.css.ts b/src/app/components/message/Time.css.ts
new file mode 100644
index 00000000..f2f5ac55
--- /dev/null
+++ b/src/app/components/message/Time.css.ts
@@ -0,0 +1,6 @@
+import { style } from '@vanilla-extract/css';
+
+export const Time = style({
+ fontWeight: 'inherit',
+ flexShrink: 0,
+});
diff --git a/src/app/components/message/Time.tsx b/src/app/components/message/Time.tsx
index 3eab5cc2..bc5e487a 100644
--- a/src/app/components/message/Time.tsx
+++ b/src/app/components/message/Time.tsx
@@ -1,12 +1,15 @@
import React, { ComponentProps } from 'react';
import { Text, as } from 'folds';
+import classNames from 'classnames';
import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../utils/time';
+import * as css from './Time.css';
export type TimeProps = {
compact?: boolean;
ts: number;
hour24Clock: boolean;
dateFormatString: string;
+ inheritPriority?: boolean;
};
/**
@@ -22,7 +25,7 @@ export type TimeProps = {
* @returns {React.ReactElement} A element with the formatted date/time.
*/
export const Time = as<'span', TimeProps & ComponentProps>(
- ({ compact, hour24Clock, dateFormatString, ts, ...props }, ref) => {
+ ({ compact, hour24Clock, dateFormatString, ts, inheritPriority, className, ...props }, ref) => {
const formattedTime = timeHourMinute(ts, hour24Clock);
let time = '';
@@ -33,11 +36,18 @@ export const Time = as<'span', TimeProps & ComponentProps>(
} else if (yesterday(ts)) {
time = `Yesterday ${formattedTime}`;
} else {
- time = `${timeDayMonYear(ts, dateFormatString)} ${formattedTime}`;
+ time = `${timeDayMonYear(ts, dateFormatString)}, ${formattedTime}`;
}
return (
-
+
{time}
);
diff --git a/src/app/components/message/content/AudioContent.tsx b/src/app/components/message/content/AudioContent.tsx
index 478486a4..71551b12 100644
--- a/src/app/components/message/content/AudioContent.tsx
+++ b/src/app/components/message/content/AudioContent.tsx
@@ -54,8 +54,7 @@ export function AudioContent({
const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => {
- const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
- if (!mediaUrl) throw new Error('Invalid media URL');
+ const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);
diff --git a/src/app/components/message/content/FileContent.tsx b/src/app/components/message/content/FileContent.tsx
index 7e127f2a..ad54c531 100644
--- a/src/app/components/message/content/FileContent.tsx
+++ b/src/app/components/message/content/FileContent.tsx
@@ -86,8 +86,7 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
const [textState, loadText] = useAsyncCallback(
useCallback(async () => {
- const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
- if (!mediaUrl) throw new Error('Invalid media URL');
+ const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);
@@ -177,8 +176,7 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
const [pdfState, loadPdf] = useAsyncCallback(
useCallback(async () => {
- const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
- if (!mediaUrl) throw new Error('Invalid media URL');
+ const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);
@@ -255,8 +253,7 @@ export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFil
const [downloadState, download] = useAsyncCallback(
useCallback(async () => {
- const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
- if (!mediaUrl) throw new Error('Invalid media URL');
+ const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);
diff --git a/src/app/components/message/content/ImageContent.tsx b/src/app/components/message/content/ImageContent.tsx
index 4f1d9c75..84e3709e 100644
--- a/src/app/components/message/content/ImageContent.tsx
+++ b/src/app/components/message/content/ImageContent.tsx
@@ -87,8 +87,7 @@ export const ImageContent = as<'div', ImageContentProps>(
const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => {
- const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
- if (!mediaUrl) throw new Error('Invalid media URL');
+ const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
if (encInfo) {
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo)
diff --git a/src/app/components/message/content/ThumbnailContent.tsx b/src/app/components/message/content/ThumbnailContent.tsx
index aa9171aa..0746137c 100644
--- a/src/app/components/message/content/ThumbnailContent.tsx
+++ b/src/app/components/message/content/ThumbnailContent.tsx
@@ -23,8 +23,7 @@ export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) {
throw new Error('Failed to load thumbnail');
}
- const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication);
- if (!mediaUrl) throw new Error('Invalid media URL');
+ const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? thumbMxcUrl;
if (encInfo) {
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo)
diff --git a/src/app/components/message/content/VideoContent.tsx b/src/app/components/message/content/VideoContent.tsx
index b33ec272..52073ac1 100644
--- a/src/app/components/message/content/VideoContent.tsx
+++ b/src/app/components/message/content/VideoContent.tsx
@@ -81,8 +81,7 @@ export const VideoContent = as<'div', VideoContentProps>(
const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => {
- const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
- if (!mediaUrl) throw new Error('Invalid media URL');
+ const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
decryptFile(encBuf, mimeType, encInfo)
diff --git a/src/app/components/page/style.css.ts b/src/app/components/page/style.css.ts
index b106320f..bd14cd58 100644
--- a/src/app/components/page/style.css.ts
+++ b/src/app/components/page/style.css.ts
@@ -111,7 +111,6 @@ export const PageHeroSection = style([
},
]);
-
export const PageContentCenter = style([
DefaultReset,
{
diff --git a/src/app/components/room-avatar/RoomAvatar.tsx b/src/app/components/room-avatar/RoomAvatar.tsx
index cbcd626a..23f3998d 100644
--- a/src/app/components/room-avatar/RoomAvatar.tsx
+++ b/src/app/components/room-avatar/RoomAvatar.tsx
@@ -2,7 +2,7 @@ import { JoinRule } from 'matrix-js-sdk';
import { AvatarFallback, AvatarImage, Icon, Icons, color } from 'folds';
import React, { ComponentProps, ReactEventHandler, ReactNode, forwardRef, useState } from 'react';
import * as css from './RoomAvatar.css';
-import { getRoomIconSrc } from '../../utils/room';
+import { joinRuleToIconSrc } from '../../utils/room';
import colorMXID from '../../../util/colorMXID';
type RoomAvatarProps = {
@@ -44,9 +44,13 @@ export function RoomAvatar({ roomId, src, alt, renderFallback }: RoomAvatarProps
export const RoomIcon = forwardRef<
SVGSVGElement,
Omit, 'src'> & {
- joinRule?: JoinRule;
- roomType?: string;
+ joinRule: JoinRule;
+ space?: boolean;
}
->(({ joinRule, roomType, ...props }, ref) => (
-
+>(({ joinRule, space, ...props }, ref) => (
+
));
diff --git a/src/app/components/room-card/RoomCard.tsx b/src/app/components/room-card/RoomCard.tsx
index 64aaaacb..34a7e24b 100644
--- a/src/app/components/room-card/RoomCard.tsx
+++ b/src/app/components/room-card/RoomCard.tsx
@@ -1,5 +1,5 @@
import React, { ReactNode, useCallback, useRef, useState } from 'react';
-import { MatrixError } from 'matrix-js-sdk';
+import { MatrixError, Room } from 'matrix-js-sdk';
import {
Avatar,
Badge,
@@ -31,7 +31,6 @@ import { RoomType, StateEvent } from '../../../types/matrix/room';
import { useJoinedRoomId } from '../../hooks/useJoinedRoomId';
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
import { getRoomAvatarUrl, getStateEvent } from '../../utils/room';
-import { joinEstateRoomOrThrow } from '../../features/estate/joinEstateRoom';
import { useStateEventCallback } from '../../hooks/useStateEventCallback';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
@@ -198,10 +197,8 @@ export const RoomCard = as<'div', RoomCardProps>(
)
);
- const [joinState, join] = useAsyncCallback(
- useCallback(async () => {
- await joinEstateRoomOrThrow(mx, roomIdOrAlias, viaServers);
- }, [mx, roomIdOrAlias, viaServers])
+ const [joinState, join] = useAsyncCallback(
+ useCallback(() => mx.joinRoom(roomIdOrAlias, { viaServers }), [mx, roomIdOrAlias, viaServers])
);
const joining =
joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success;
diff --git a/src/app/components/room-intro/RoomIntro.tsx b/src/app/components/room-intro/RoomIntro.tsx
index 2e4e3ebc..ce550992 100644
--- a/src/app/components/room-intro/RoomIntro.tsx
+++ b/src/app/components/room-intro/RoomIntro.tsx
@@ -17,7 +17,6 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { InviteUserPrompt } from '../invite-user-prompt';
-import { joinEstateRoomOrThrow } from '../../features/estate/joinEstateRoom';
export type RoomIntroProps = {
room: Room;
@@ -44,7 +43,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
const prevRoomId = createContent?.predecessor?.room_id;
const [prevRoomState, joinPrevRoom] = useAsyncCallback(
- useCallback((roomId: string) => joinEstateRoomOrThrow(mx, roomId), [mx])
+ useCallback(async (roomId: string) => mx.joinRoom(roomId), [mx])
);
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
diff --git a/src/app/components/sequence-card/SequenceCard.tsx b/src/app/components/sequence-card/SequenceCard.tsx
index 46d5ea0f..d0e77ae6 100644
--- a/src/app/components/sequence-card/SequenceCard.tsx
+++ b/src/app/components/sequence-card/SequenceCard.tsx
@@ -17,7 +17,6 @@ export const SequenceCard = as<
firstChild,
lastChild,
outlined,
- mergeBorder,
...props
},
ref
@@ -25,7 +24,7 @@ export const SequenceCard = as<
(
- ({ size, variant, className, ...props }, ref) => (
-
- )
-);
diff --git a/src/app/components/stacked-avatar/index.ts b/src/app/components/stacked-avatar/index.ts
deleted file mode 100644
index 63d9f56d..00000000
--- a/src/app/components/stacked-avatar/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './StackedAvatar';
diff --git a/src/app/components/stacked-avatar/styles.css.ts b/src/app/components/stacked-avatar/styles.css.ts
deleted file mode 100644
index 3bbf7271..00000000
--- a/src/app/components/stacked-avatar/styles.css.ts
+++ /dev/null
@@ -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;
diff --git a/src/app/components/uia-stages/SSOStage.tsx b/src/app/components/uia-stages/SSOStage.tsx
index e3d6126a..f85bcb30 100644
--- a/src/app/components/uia-stages/SSOStage.tsx
+++ b/src/app/components/uia-stages/SSOStage.tsx
@@ -26,12 +26,7 @@ export function SSOStage({
useEffect(() => {
const handleMessage = (evt: MessageEvent) => {
- if (
- evt.origin === new URL(ssoRedirectURL).origin &&
- ssoWindow &&
- evt.data === 'authDone' &&
- evt.source === ssoWindow
- ) {
+ if (ssoWindow && evt.data === 'authDone' && evt.source === ssoWindow) {
ssoWindow.close();
setSSOWindow(undefined);
handleSubmit();
@@ -42,7 +37,7 @@ export function SSOStage({
return () => {
window.removeEventListener('message', handleMessage);
};
- }, [ssoWindow, handleSubmit, ssoRedirectURL]);
+ }, [ssoWindow, handleSubmit]);
return (
diff --git a/src/app/components/url-preview/UrlPreview.css.tsx b/src/app/components/url-preview/UrlPreview.css.tsx
index cd0b2528..3192dc49 100644
--- a/src/app/components/url-preview/UrlPreview.css.tsx
+++ b/src/app/components/url-preview/UrlPreview.css.tsx
@@ -23,11 +23,6 @@ export const UrlPreviewImg = style([
objectPosition: 'center',
flexShrink: 0,
overflow: 'hidden',
- cursor: 'pointer',
-
- ':hover': {
- filter: 'brightness(0.8)',
- },
},
]);
diff --git a/src/app/components/url-preview/UrlPreviewCard.tsx b/src/app/components/url-preview/UrlPreviewCard.tsx
index ee8967af..2a51969a 100644
--- a/src/app/components/url-preview/UrlPreviewCard.tsx
+++ b/src/app/components/url-preview/UrlPreviewCard.tsx
@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { IPreviewUrlResponse } from 'matrix-js-sdk';
import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds';
-import { ImageOverlay } from '../ImageOverlay';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview';
@@ -13,8 +12,6 @@ import * as css from './UrlPreviewCard.css';
import { tryDecodeURIComponent } from '../../utils/dom';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
-import { ImageViewer } from '../image-viewer';
-import { onEnterOrSpace } from '../../utils/keyboard';
const linkStyles = { color: color.Success.Main };
@@ -22,7 +19,6 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
({ url, ts, ...props }, ref) => {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
- const [viewer, setViewer] = useState(false);
const [previewStatus, loadPreview] = useAsyncCallback(
useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
);
@@ -34,41 +30,11 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
if (previewStatus.status === AsyncStatus.Error) return null;
const renderContent = (prev: IPreviewUrlResponse) => {
- const thumbUrl = mxcUrlToHttp(
- mx,
- prev['og:image'] || '',
- useAuthentication,
- 256,
- 256,
- 'scale',
- false
- );
-
- const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication);
+ const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication, 256, 256, 'scale', false);
return (
<>
- {thumbUrl && (
- onEnterOrSpace(() => setViewer(true))(evt)}
- onClick={() => setViewer(true)}
- />
- )}
- {imgUrl && (
- {
- setViewer(false);
- }}
- renderViewer={(p) => }
- />
- )}
+ {imgUrl && }
(
as="a"
href={url}
target="_blank"
- rel="noreferrer"
+ rel="no-referrer"
size="T200"
priority="300"
>
diff --git a/src/app/components/user-profile/UserChips.tsx b/src/app/components/user-profile/UserChips.tsx
index c65e2cef..53e6618b 100644
--- a/src/app/components/user-profile/UserChips.tsx
+++ b/src/app/components/user-profile/UserChips.tsx
@@ -28,11 +28,7 @@ import { copyToClipboard } from '../../utils/dom';
import { getExploreServerPath } from '../../pages/pathUtils';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { factoryRoomIdByAtoZ } from '../../utils/sort';
-import {
- useMutualRooms,
- useMutualRoomsSupport,
- useUnstableMutualRoomsSupport,
-} from '../../hooks/useMutualRooms';
+import { useMutualRooms, useMutualRoomsSupport } from '../../hooks/useMutualRooms';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useDirectRooms } from '../../pages/client/direct/useDirectRooms';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
@@ -237,9 +233,7 @@ type MutualRoomsData = {
export function MutualRoomsChip({ userId }: { userId: string }) {
const mx = useMatrixClient();
const mutualRoomSupported = useMutualRoomsSupport();
- const mutualRoomUnstable = useUnstableMutualRoomsSupport();
const mutualRoomsState = useMutualRooms(userId);
- console.log(mutualRoomSupported, mutualRoomsState);
const { navigateRoom, navigateSpace } = useRoomNavigate();
const closeUserRoomProfile = useCloseUserRoomProfile();
const directs = useDirectRooms();
@@ -285,7 +279,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) {
if (
userId === mx.getSafeUserId() ||
- (!mutualRoomSupported && !mutualRoomUnstable) ||
+ !mutualRoomSupported ||
mutualRoomsState.status === AsyncStatus.Error
) {
return null;
@@ -329,7 +323,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) {
)}
/>
) : (
-
+
)}
}
diff --git a/src/app/cs-api.ts b/src/app/cs-api.ts
index 95a131a8..b9aac06a 100644
--- a/src/app/cs-api.ts
+++ b/src/app/cs-api.ts
@@ -20,16 +20,6 @@ export type AutoDiscoveryInfo = Record & {
'm.identity_server'?: {
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 (
diff --git a/src/app/features/add-existing/AddExisting.tsx b/src/app/features/add-existing/AddExisting.tsx
index d100096b..cbae018f 100644
--- a/src/app/features/add-existing/AddExisting.tsx
+++ b/src/app/features/add-existing/AddExisting.tsx
@@ -291,11 +291,7 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM
)}
/>
) : (
-
+
)}
}
diff --git a/src/app/features/call-status/CallControl.tsx b/src/app/features/call-status/CallControl.tsx
deleted file mode 100644
index 1808be92..00000000
--- a/src/app/features/call-status/CallControl.tsx
+++ /dev/null
@@ -1,235 +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;
- disabled?: boolean;
-};
-function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps) {
- const [micState, toggleMic] = useAsyncCallback(onToggle);
- const loading = micState.status === AsyncStatus.Loading;
-
- return (
-
- {enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}
-
- }
- >
- {(anchorRef) => (
-
-
-
- )}
-
- );
-}
-
-type SoundButtonProps = {
- enabled: boolean;
- onToggle: () => void;
- disabled?: boolean;
-};
-function SoundButton({ enabled, onToggle, disabled }: SoundButtonProps) {
- return (
-
- {enabled ? 'Turn Off Sound' : 'Turn On Sound'}
-
- }
- >
- {(anchorRef) => (
- onToggle()}
- outlined
- disabled={disabled}
- >
-
-
- )}
-
- );
-}
-
-type VideoButtonProps = {
- enabled: boolean;
- onToggle: () => Promise;
- disabled?: boolean;
-};
-function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
- const [videoState, toggleVideo] = useAsyncCallback(onToggle);
- const loading = videoState.status === AsyncStatus.Loading;
-
- return (
-
- {enabled ? 'Stop Camera' : 'Start Camera'}
-
- }
- >
- {(anchorRef) => (
-
-
-
- )}
-
- );
-}
-
-type ScreenShareButtonProps = {
- enabled: boolean;
- onToggle: () => void;
- disabled?: boolean;
-};
-function ScreenShareButton({ enabled, onToggle, disabled }: ScreenShareButtonProps) {
- return (
-
- {enabled ? 'Stop Screenshare' : 'Start Screenshare'}
-
- }
- >
- {(anchorRef) => (
-
-
-
- )}
-
- );
-}
-
-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 handleMicrophoneToggle = useCallback(() => callEmbed.control.toggleMicrophone(), [callEmbed]);
- const handleVideoToggle = useCallback(() => callEmbed.control.toggleVideo(), [callEmbed]);
-
- 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 (
-
-
-
- callEmbed.control.toggleSound()}
- disabled={!callJoined}
- />
- {!compact && }
-
- {!compact && (
- callEmbed.control.toggleScreenshare()}
- disabled={!callJoined}
- />
- )}
-
-
-
- ) : (
-
- )
- }
- disabled={exiting}
- outlined
- onClick={handleHangup}
- >
- {!compact && (
-
- End
-
- )}
-
-
- );
-}
diff --git a/src/app/features/call-status/CallRoomName.tsx b/src/app/features/call-status/CallRoomName.tsx
deleted file mode 100644
index 8b65528a..00000000
--- a/src/app/features/call-status/CallRoomName.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import React from 'react';
-import { Room } from 'matrix-js-sdk';
-import { Chip, Icon, Icons, 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 (
-
- ) : (
-
- )
- }
- onClick={() => navigateRoom(room.roomId)}
- >
-
- {name}
- {!dm && perfectOrphanParent && (
-
- {' β’'} {getRoom(perfectOrphanParent)?.name ?? perfectOrphanParent}
-
- )}
-
-
- );
-}
diff --git a/src/app/features/call-status/CallStatus.tsx b/src/app/features/call-status/CallStatus.tsx
deleted file mode 100644
index 1ff3b837..00000000
--- a/src/app/features/call-status/CallStatus.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import React from 'react';
-import { Box, Spinner } from 'folds';
-import classNames from 'classnames';
-import { LiveChip } from './LiveChip';
-import * as css from './styles.css';
-import { CallRoomName } from './CallRoomName';
-import { CallControl } from './CallControl';
-import { ContainerColor } from '../../styles/ContainerColor.css';
-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(callSession);
- const screenSize = useScreenSize();
- const callJoined = useCallJoined(callEmbed);
- const speakers = useCallSpeakers(callEmbed);
-
- const compact = screenSize === ScreenSize.Mobile;
-
- const memberVisible = callJoined && callMembers.length > 0;
-
- return (
-
-
- {memberVisible ? (
-
-
-
- ) : (
-
- )}
-
- {!compact && (
- <>
-
- {speakers.size > 0 && (
- <>
-
-
-
- >
- )}
- >
- )}
-
- {memberVisible && (
-
-
-
- )}
-
- {memberVisible && !compact && }
-
- {compact && (
-
-
-
- )}
-
-
-
- );
-}
diff --git a/src/app/features/call-status/LiveChip.tsx b/src/app/features/call-status/LiveChip.tsx
deleted file mode 100644
index 32df3289..00000000
--- a/src/app/features/call-status/LiveChip.tsx
+++ /dev/null
@@ -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();
-
- const handleOpenMenu: MouseEventHandler = (evt) => {
- setCords(evt.currentTarget.getBoundingClientRect());
- };
-
- return (
- setCords(undefined),
- clickOutsideDeactivates: true,
- isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
- isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
- escapeDeactivates: stopPropagation,
- }}
- >
-
-
-
-
- {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 (
-
- openUserProfile(
- room.roomId,
- undefined,
- userId,
- getMouseEventCords(evt.nativeEvent),
- 'Right'
- )
- }
- before={
-
- }
- />
-
- }
- >
-
- {name}
-
-
- );
- })}
-
-
-
-
-
- }
- >
- }
- after={ }
- radii="Pill"
- onClick={handleOpenMenu}
- >
-
- {count} Live
-
-
-
- );
-}
diff --git a/src/app/features/call-status/MemberGlance.tsx b/src/app/features/call-status/MemberGlance.tsx
deleted file mode 100644
index dabc440f..00000000
--- a/src/app/features/call-status/MemberGlance.tsx
+++ /dev/null
@@ -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;
- 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 (
-
- {visibleMembers.map((callMember) => {
- const { userId } = callMember;
- 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 (
-
- openUserProfile(
- room.roomId,
- undefined,
- userId,
- getMouseEventCords(evt.nativeEvent),
- 'Top'
- )
- }
- >
- }
- />
-
- );
- })}
- {remainingCount > 0 && (
-
- +{remainingCount}
-
- )}
-
- );
-}
diff --git a/src/app/features/call-status/MemberSpeaking.tsx b/src/app/features/call-status/MemberSpeaking.tsx
deleted file mode 100644
index 27e272f2..00000000
--- a/src/app/features/call-status/MemberSpeaking.tsx
+++ /dev/null
@@ -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;
-};
-export function MemberSpeaking({ room, speakers }: MemberSpeakingProps) {
- const speakingNames = Array.from(speakers).map(
- (userId) => getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId
- );
- return (
-
-
-
- {speakingNames.length === 1 && (
- <>
- {speakingNames[0]}
-
- {' is speaking...'}
-
- >
- )}
- {speakingNames.length === 2 && (
- <>
- {speakingNames[0]}
-
- {' and '}
-
- {speakingNames[1]}
-
- {' are speaking...'}
-
- >
- )}
- {speakingNames.length === 3 && (
- <>
- {speakingNames[0]}
-
- {', '}
-
- {speakingNames[1]}
-
- {' and '}
-
- {speakingNames[2]}
-
- {' are speaking...'}
-
- >
- )}
- {speakingNames.length > 3 && (
- <>
- {speakingNames[0]}
-
- {', '}
-
- {speakingNames[1]}
-
- {', '}
-
- {speakingNames[2]}
-
- {' and '}
-
- {speakingNames.length - 3} others
-
- {' are speaking...'}
-
- >
- )}
-
-
- );
-}
diff --git a/src/app/features/call-status/components.tsx b/src/app/features/call-status/components.tsx
deleted file mode 100644
index cd0c0daf..00000000
--- a/src/app/features/call-status/components.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react';
-import { Line } from 'folds';
-import * as css from './styles.css';
-
-export function StatusDivider() {
- return (
-
- );
-}
diff --git a/src/app/features/call-status/index.ts b/src/app/features/call-status/index.ts
deleted file mode 100644
index 99accf8b..00000000
--- a/src/app/features/call-status/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './CallStatus';
diff --git a/src/app/features/call-status/styles.css.ts b/src/app/features/call-status/styles.css.ts
deleted file mode 100644
index dd32bc3d..00000000
--- a/src/app/features/call-status/styles.css.ts
+++ /dev/null
@@ -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}`,
-});
diff --git a/src/app/features/call/CallControls.tsx b/src/app/features/call/CallControls.tsx
deleted file mode 100644
index 469b47a8..00000000
--- a/src/app/features/call/CallControls.tsx
+++ /dev/null
@@ -1,206 +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(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();
-
- const handleOpenMenu: MouseEventHandler = (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 handleMicrophoneToggle = useCallback(() => callEmbed.control.toggleMicrophone(), [callEmbed]);
- const handleVideoToggle = useCallback(() => callEmbed.control.toggleVideo(), [callEmbed]);
-
- const [hangupState, hangup] = useAsyncCallback(
- useCallback(() => callEmbed.hangup(), [callEmbed])
- );
- const exiting =
- hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success;
-
- return (
-
-
-
-
-
- callEmbed.control.toggleSound()} />
-
- {!compact && }
-
-
- callEmbed.control.toggleScreenshare()}
- />
-
-
- {!compact && }
-
-
-
- setCords(undefined),
- clickOutsideDeactivates: true,
- isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
- isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
- escapeDeactivates: stopPropagation,
- }}
- >
-
-
-
-
- {spotlight ? 'Grid View' : 'Spotlight View'}
-
-
-
-
- Reactions
-
-
-
-
- Settings
-
-
-
-
-
- }
- >
-
-
-
-
-
-
-
- ) : (
-
- )
- }
- disabled={exiting}
- >
- End
-
-
-
-
-
- );
-}
diff --git a/src/app/features/call/CallMemberCard.tsx b/src/app/features/call/CallMemberCard.tsx
deleted file mode 100644
index 8c37160b..00000000
--- a/src/app/features/call/CallMemberCard.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import { CallMembership } 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';
-
-type CallMemberCardProps = {
- member: CallMembership;
-};
-export function CallMemberCard({ member }: CallMemberCardProps) {
- const mx = useMatrixClient();
- const useAuthentication = useMediaAuthentication();
- const room = useRoom();
-
- const openUserProfile = useOpenUserRoomProfile();
-
- const { userId } = member;
- 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.callIntent === 'audio';
-
- return (
-
- openUserProfile(
- room.roomId,
- undefined,
- userId,
- getMouseEventCords(evt.nativeEvent),
- 'Right'
- )
- }
- >
-
-
- }
- />
-
-
-
- {name}
-
-
- {audioOnly && }
-
-
- );
-}
-
-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) => (
-
- ))}
- {members.length > max && (
- setViewMore(!viewMore)}
- >
-
- {viewMore ? (
-
- Collapse
-
- ) : (
-
- {remaining === 0 ? `+${remaining} Other` : `+${remaining} Others`}
-
- )}
-
-
-
- )}
- >
- );
-}
diff --git a/src/app/features/call/CallView.tsx b/src/app/features/call/CallView.tsx
deleted file mode 100644
index 1fb97650..00000000
--- a/src/app/features/call/CallView.tsx
+++ /dev/null
@@ -1,173 +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 { ContainerColor } from '../../styles/ContainerColor.css';
-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';
-import { webRTCSupported } from '../../utils/rtc';
-
-function LivekitServerMissingMessage() {
- return (
-
- Your homeserver does not support calling.
-
- );
-}
-
-function WebRTCMissingError() {
- return (
-
- Your browser does not support WebRTC, which is required for calling.
-
- );
-}
-
-function JoinMessage({
- hasParticipant,
- livekitSupported,
- rtcSupported,
-}: {
- hasParticipant?: boolean;
- livekitSupported?: boolean;
- rtcSupported?: boolean;
-}) {
- if (rtcSupported === false) {
- return ;
- }
-
- if (livekitSupported === false) {
- return ;
- }
-
- if (hasParticipant) return null;
-
- return (
-
- Voice chatβs empty β Be the first to hop in!
-
- );
-}
-
-function NoPermissionMessage() {
- return (
-
- You don't have permission to join!
-
- );
-}
-
-function AlreadyInCallMessage() {
- return (
-
- Already in another call β End the current call to join!
-
- );
-}
-
-function CallPrescreen() {
- const mx = useMatrixClient();
- const room = useRoom();
- const livekitSupported = useLivekitSupport();
- const rtcSupported = webRTCSupported();
-
- const powerLevels = usePowerLevelsContext();
- const creators = useRoomCreators(room);
-
- const permissions = useRoomPermissions(creators, powerLevels);
- const hasPermission = permissions.stateEvent(
- StateEvent.GroupCallMemberPrefix,
- mx.getSafeUserId()
- );
-
- const callSession = useCallSession(room);
- const callMembers = useCallMembers(callSession);
- const hasParticipant = callMembers.length > 0;
-
- const callEmbed = useCallEmbed();
- const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
-
- const canJoin = hasPermission && livekitSupported && rtcSupported;
-
- return (
-
-
-
- {hasParticipant && (
-
-
- Participant
-
-
-
- {callMembers.length} Live
-
-
-
- )}
-
-
-
- {!inOtherCall &&
- (hasPermission ? (
-
- ) : (
-
- ))}
- {inOtherCall && }
-
-
-
-
- );
-}
-
-type CallJoinedProps = {
- containerRef: RefObject;
- joined: boolean;
-};
-function CallJoined({ joined, containerRef }: CallJoinedProps) {
- const callEmbed = useCallEmbed();
-
- return (
-
-
- {callEmbed && joined && }
-
- );
-}
-
-export function CallView() {
- const room = useRoom();
- const callContainerRef = useRef(null);
- useCallEmbedPlacementSync(callContainerRef);
-
- const callEmbed = useCallEmbed();
- const callJoined = useCallJoined(callEmbed);
-
- const currentJoined = callEmbed?.roomId === room.roomId && callJoined;
-
- return (
-
- {!currentJoined && }
-
-
- );
-}
diff --git a/src/app/features/call/Controls.tsx b/src/app/features/call/Controls.tsx
deleted file mode 100644
index 6aa94ff5..00000000
--- a/src/app/features/call/Controls.tsx
+++ /dev/null
@@ -1,186 +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';
-import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
-
-export function ControlDivider() {
- return (
-
- );
-}
-
-type MicrophoneButtonProps = {
- enabled: boolean;
- onToggle: () => Promise;
-};
-export function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
- const [micState, toggleMic] = useAsyncCallback(onToggle);
- const loading = micState.status === AsyncStatus.Loading;
-
- return (
-
- {enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}
-
- }
- >
- {(anchorRef) => (
-
-
-
- )}
-
- );
-}
-
-type SoundButtonProps = {
- enabled: boolean;
- onToggle: () => void;
-};
-export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
- return (
-
- {enabled ? 'Turn Off Sound' : 'Turn On Sound'}
-
- }
- >
- {(anchorRef) => (
- onToggle()}
- outlined
- >
-
-
- )}
-
- );
-}
-
-type VideoButtonProps = {
- enabled: boolean;
- onToggle: () => Promise;
-};
-export function VideoButton({ enabled, onToggle }: VideoButtonProps) {
- const [videoState, toggleVideo] = useAsyncCallback(onToggle);
- const loading = videoState.status === AsyncStatus.Loading;
-
- return (
-
- {enabled ? 'Stop Camera' : 'Start Camera'}
-
- }
- >
- {(anchorRef) => (
-
-
-
- )}
-
- );
-}
-
-type ScreenShareButtonProps = {
- enabled: boolean;
- onToggle: () => void;
-};
-export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
- return (
-
- {enabled ? 'Stop Screenshare' : 'Start Screenshare'}
-
- }
- >
- {(anchorRef) => (
- onToggle()}
- outlined
- >
-
-
- )}
-
- );
-}
-
-export function ChatButton() {
- const [chat, setChat] = useAtom(callChatAtom);
-
- return (
-
- {chat ? 'Close Chat' : 'Open Chat'}
-
- }
- >
- {(anchorRef) => (
- setChat(!chat)}
- outlined
- >
-
-
- )}
-
- );
-}
diff --git a/src/app/features/call/PrescreenControls.tsx b/src/app/features/call/PrescreenControls.tsx
deleted file mode 100644
index ec779b83..00000000
--- a/src/app/features/call/PrescreenControls.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import React, { useCallback } 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();
-
-
- const handleMicrophoneToggle = useCallback(async () => toggleMicrophone(), [toggleMicrophone]);
- const handleVideoToggle = useCallback(async () => toggleVideo(), [toggleVideo]);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- startCall(room, { microphone, video, sound })}
- disabled={disabled || joining}
- before={
- joining ? (
-
- ) : (
-
- )
- }
- >
- Join
-
-
-
- );
-}
diff --git a/src/app/features/call/styles.css.ts b/src/app/features/call/styles.css.ts
deleted file mode 100644
index 2b9f28ad..00000000
--- a/src/app/features/call/styles.css.ts
+++ /dev/null
@@ -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,
-});
diff --git a/src/app/features/common-settings/general/RoomJoinRules.tsx b/src/app/features/common-settings/general/RoomJoinRules.tsx
index 0d352167..b9e75499 100644
--- a/src/app/features/common-settings/general/RoomJoinRules.tsx
+++ b/src/app/features/common-settings/general/RoomJoinRules.tsx
@@ -6,8 +6,9 @@ import { useAtomValue } from 'jotai';
import {
ExtendedJoinRules,
JoinRulesSwitcher,
- useJoinRuleIcons,
+ useRoomJoinRuleIcon,
useRoomJoinRuleLabel,
+ useSpaceJoinRuleIcon,
} from '../../../components/JoinRulesSwitcher';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css';
@@ -74,7 +75,8 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
return r;
}, [allowKnockRestricted, allowRestricted, allowKnock, space]);
- const icons = useJoinRuleIcons(room.getType());
+ const icons = useRoomJoinRuleIcon();
+ const spaceIcons = useSpaceJoinRuleIcon();
const labels = useRoomJoinRuleLabel();
const [submitState, submit] = useAsyncCallback(
@@ -135,7 +137,7 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
}
after={
(
(
(null);
const searchInputRef = useRef(null);
diff --git a/src/app/features/create-room/CreateRoom.tsx b/src/app/features/create-room/CreateRoom.tsx
index 94799f6e..6ad469c9 100644
--- a/src/app/features/create-room/CreateRoom.tsx
+++ b/src/app/features/create-room/CreateRoom.tsx
@@ -1,5 +1,5 @@
import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
-import { MatrixError, Room, JoinRule } from 'matrix-js-sdk';
+import { MatrixError, Room } from 'matrix-js-sdk';
import {
Box,
Button,
@@ -33,43 +33,24 @@ import {
createRoom,
CreateRoomAliasInput,
CreateRoomData,
- CreateRoomAccess,
- CreateRoomAccessSelector,
+ CreateRoomKind,
+ CreateRoomKindSelector,
RoomVersionSelector,
useAdditionalCreators,
- CreateRoomType,
} 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 isVoiceRoom = type === CreateRoomType.VoiceRoom;
-
- let joinRule: JoinRule = JoinRule.Public;
- 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;
+const getCreateRoomKindToIcon = (kind: CreateRoomKind) => {
+ if (kind === CreateRoomKind.Private) return Icons.HashLock;
+ if (kind === CreateRoomKind.Restricted) return Icons.Hash;
+ return Icons.HashGlobe;
};
type CreateRoomFormProps = {
- defaultAccess?: CreateRoomAccess;
- defaultType?: CreateRoomType;
+ defaultKind?: CreateRoomKind;
space?: Room;
onCreate?: (roomId: string) => void;
};
-export function CreateRoomForm({
- defaultAccess,
- defaultType,
- space,
- onCreate,
-}: CreateRoomFormProps) {
+export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormProps) {
const mx = useMatrixClient();
const alive = useAlive();
@@ -83,9 +64,8 @@ export function CreateRoomForm({
const allowRestricted = space && restrictedSupported(selectedRoomVersion);
- const [type, setType] = useState(defaultType ?? CreateRoomType.TextRoom);
- const [access, setAccess] = useState(
- defaultAccess ?? (allowRestricted ? CreateRoomAccess.Restricted : CreateRoomAccess.Private)
+ const [kind, setKind] = useState(
+ defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private
);
const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } =
@@ -95,13 +75,13 @@ export function CreateRoomForm({
const [knock, setKnock] = useState(false);
const [advance, setAdvance] = useState(false);
- const allowKnock = access === CreateRoomAccess.Private && knockSupported(selectedRoomVersion);
+ const allowKnock = kind === CreateRoomKind.Private && knockSupported(selectedRoomVersion);
const allowKnockRestricted =
- access === CreateRoomAccess.Restricted && knockRestrictedSupported(selectedRoomVersion);
+ kind === CreateRoomKind.Restricted && knockRestrictedSupported(selectedRoomVersion);
const handleRoomVersionChange = (version: string) => {
if (!restrictedSupported(version)) {
- setAccess(CreateRoomAccess.Private);
+ setKind(CreateRoomKind.Private);
}
selectRoomVersion(version);
};
@@ -127,23 +107,19 @@ export function CreateRoomForm({
aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined;
if (!roomName) return;
- const publicRoom = access === CreateRoomAccess.Public;
+ const publicRoom = kind === CreateRoomKind.Public;
let roomKnock = false;
- if (allowKnock && access === CreateRoomAccess.Private) {
+ if (allowKnock && kind === CreateRoomKind.Private) {
roomKnock = knock;
}
- if (allowKnockRestricted && access === CreateRoomAccess.Restricted) {
+ if (allowKnockRestricted && kind === CreateRoomKind.Restricted) {
roomKnock = knock;
}
- let roomType: RoomType | undefined;
- if (type === CreateRoomType.VoiceRoom) roomType = RoomType.Call;
-
create({
version: selectedRoomVersion,
- type: roomType,
parent: space,
- access,
+ kind,
name: roomName,
topic: roomTopic || undefined,
aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
@@ -160,32 +136,21 @@ export function CreateRoomForm({
return (
- {!space && (
-
- Type
-
-
- )}
Access
- getCreateRoomAccessToIcon(roomAccess, type)}
+ getIcon={getCreateRoomKindToIcon}
/>
Name
}
+ before={ }
name="nameInput"
autoFocus
size="500"
@@ -206,7 +171,7 @@ export function CreateRoomForm({
/>
- {access === CreateRoomAccess.Public && }
+ {kind === CreateRoomKind.Public && }
@@ -218,7 +183,7 @@ export function CreateRoomForm({
onClick={() => setAdvance(!advance)}
type="button"
>
- Advanced Options
+ Advance Options
@@ -236,7 +201,7 @@ export function CreateRoomForm({
/>
)}
- {access !== CreateRoomAccess.Public && (
+ {kind !== CreateRoomKind.Public && (
<>
-
- {type === CreateRoomType.VoiceRoom ? 'New Voice Room' : 'New Chat Room'}
-
+ New Room
@@ -77,7 +74,7 @@ function CreateRoomModal({ state }: CreateRoomModalProps) {
direction="Column"
gap="500"
>
-
+
diff --git a/src/app/features/create-space/CreateSpace.tsx b/src/app/features/create-space/CreateSpace.tsx
index b0c12f56..b1f9f649 100644
--- a/src/app/features/create-space/CreateSpace.tsx
+++ b/src/app/features/create-space/CreateSpace.tsx
@@ -33,25 +33,25 @@ import {
createRoom,
CreateRoomAliasInput,
CreateRoomData,
- CreateRoomAccess,
- CreateRoomAccessSelector,
+ CreateRoomKind,
+ CreateRoomKindSelector,
RoomVersionSelector,
useAdditionalCreators,
} from '../../components/create-room';
import { RoomType } from '../../../types/matrix/room';
-const getCreateSpaceAccessToIcon = (access: CreateRoomAccess) => {
- if (access === CreateRoomAccess.Private) return Icons.SpaceLock;
- if (access === CreateRoomAccess.Restricted) return Icons.Space;
+const getCreateSpaceKindToIcon = (kind: CreateRoomKind) => {
+ if (kind === CreateRoomKind.Private) return Icons.SpaceLock;
+ if (kind === CreateRoomKind.Restricted) return Icons.Space;
return Icons.SpaceGlobe;
};
type CreateSpaceFormProps = {
- defaultAccess?: CreateRoomAccess;
+ defaultKind?: CreateRoomKind;
space?: Room;
onCreate?: (roomId: string) => void;
};
-export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceFormProps) {
+export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFormProps) {
const mx = useMatrixClient();
const alive = useAlive();
@@ -65,8 +65,8 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
const allowRestricted = space && restrictedSupported(selectedRoomVersion);
- const [access, setAccess] = useState(
- defaultAccess ?? (allowRestricted ? CreateRoomAccess.Restricted : CreateRoomAccess.Private)
+ const [kind, setKind] = useState(
+ defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private
);
const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
@@ -76,13 +76,13 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
const [knock, setKnock] = useState(false);
const [advance, setAdvance] = useState(false);
- const allowKnock = access === CreateRoomAccess.Private && knockSupported(selectedRoomVersion);
+ const allowKnock = kind === CreateRoomKind.Private && knockSupported(selectedRoomVersion);
const allowKnockRestricted =
- access === CreateRoomAccess.Restricted && knockRestrictedSupported(selectedRoomVersion);
+ kind === CreateRoomKind.Restricted && knockRestrictedSupported(selectedRoomVersion);
const handleRoomVersionChange = (version: string) => {
if (!restrictedSupported(version)) {
- setAccess(CreateRoomAccess.Private);
+ setKind(CreateRoomKind.Private);
}
selectRoomVersion(version);
};
@@ -108,12 +108,12 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined;
if (!roomName) return;
- const publicRoom = access === CreateRoomAccess.Public;
+ const publicRoom = kind === CreateRoomKind.Public;
let roomKnock = false;
- if (allowKnock && access === CreateRoomAccess.Private) {
+ if (allowKnock && kind === CreateRoomKind.Private) {
roomKnock = knock;
}
- if (allowKnockRestricted && access === CreateRoomAccess.Restricted) {
+ if (allowKnockRestricted && kind === CreateRoomKind.Restricted) {
roomKnock = knock;
}
@@ -121,7 +121,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
version: selectedRoomVersion,
type: RoomType.Space,
parent: space,
- access,
+ kind,
name: roomName,
topic: roomTopic || undefined,
aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
@@ -139,19 +139,19 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
Access
-
Name
}
+ before={ }
name="nameInput"
autoFocus
size="500"
@@ -172,7 +172,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
/>
- {access === CreateRoomAccess.Public && }
+ {kind === CreateRoomKind.Public && }
@@ -184,7 +184,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
onClick={() => setAdvance(!advance)}
type="button"
>
- Advanced Options
+ Advance Options
@@ -202,7 +202,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
/>
)}
- {access !== CreateRoomAccess.Public && advance && (allowKnock || allowKnockRestricted) && (
+ {kind !== CreateRoomKind.Public && advance && (allowKnock || allowKnockRestricted) && (
, room: EstateRoom): string => {
- const alias = getCanonicalAliasOrRoomId(mx, room.roomId);
- if (room.direct) return getDirectRoomPath(alias);
- // A Space the user has not joined has no Space route; the room still resolves from Home.
- if (room.parentId && mx.getRoom(room.parentId)?.getMyMembership() === 'join') {
- return getSpaceRoomPath(getCanonicalAliasOrRoomId(mx, room.parentId), alias);
- }
- return getHomeRoomPath(alias);
-};
-
-function JoinRow({ room }: { room: EstateRoom }) {
- const mx = useMatrixClient();
- const navigate = useNavigate();
- const [state, join] = useAsyncCallback(
- useCallback(async () => {
- const result = await activateEstateRoom(mx, room);
- if (result.kind === 'refused') throw new Error(result.message);
- // joinRoom resolves before the store holds the room; opening it now shows the invite card.
- if (!(await awaitMyJoin(mx, room.roomId, JOIN_ARRIVAL_TIMEOUT_MS))) {
- // The join is accepted; when the room arrives the row becomes a joined row by itself.
- throw new Error('Joined. The room is still on its way and will open from this list.');
- }
- navigate(estateRoomPath(mx, room));
- }, [mx, navigate, room])
- );
- const busy = state.status === AsyncStatus.Loading;
-
- return (
-
-
-
-
- {room.name}
-
-
- {state.status === AsyncStatus.Error && (
-
- {(state.error as Error).message}
-
- )}
- : }
- onClick={join}
- disabled={busy}
- aria-label={`Join ${room.name}`}
- >
- Join
-
-
-
- );
-}
-
-function EstateRow({ room }: { room: EstateRoom }) {
- const mx = useMatrixClient();
- const selectedRoomId = useSelectedRoom();
- const notificationPreferences = useRoomsNotificationPreferencesContext();
- const mxRoom = room.state === 'joined' ? mx.getRoom(room.roomId) : null;
-
- if (!mxRoom) return ;
- return (
-
- );
-}
-
-type CollapsibleProps = {
- categoryId: string;
- label: string;
- rooms: EstateRoom[];
- children: ReactNode;
-};
-function Collapsible({ categoryId, label, rooms, children }: CollapsibleProps) {
- const mobile = useScreenSizeContext() === ScreenSize.Mobile;
- const selectedRoomId = useSelectedRoom();
- const roomToUnread = useAtomValue(roomToUnreadAtom);
- const callRoomId = useCallEmbed()?.roomId;
- const [toggled, setToggled] = useAtom(useClosedNavCategoriesAtom());
-
- const active = rooms.some(
- (r) => r.roomId === selectedRoomId || r.roomId === callRoomId || roomToUnread.has(r.roomId)
- );
- // Phones start collapsed unless the section holds something live; wide screens start open.
- // A tap flips that default; the flip is remembered per device and per screen class.
- const key = makeNavCategoryId('estate', mobile ? 'phone' : 'wide', categoryId);
- const closed = (mobile && !active) !== toggled.has(key);
-
- const handleClick: MouseEventHandler = () =>
- setToggled({ type: toggled.has(key) ? 'DELETE' : 'PUT', categoryId: key });
-
- return (
-
-
-
- {label}
-
-
- {!closed && children}
-
- );
-}
-
-function Tier({ tier }: { tier: EstateTier }) {
- const allRooms = useMemo(() => [...tier.loose, ...tier.groups.flatMap((g) => g.rooms)], [tier]);
- return (
-
- {tier.loose.map((r) => (
-
- ))}
- {tier.groups.map((g) => (
-
- {g.rooms.map((r) => (
-
- ))}
-
- ))}
-
- );
-}
-
-function EntryPoints() {
- const navigate = useNavigate();
- return (
-
-
- navigate(getHomeCreatePath())}>
-
-
-
-
-
-
- Create Room
-
-
-
-
-
-
- {(open, setOpen) => (
- <>
-
- setOpen(true)}>
-
-
-
-
-
-
- Join with Address
-
-
-
-
-
- {open && (
- setOpen(false)}
- onOpen={(roomIdOrAlias, viaServers, eventId) => {
- setOpen(false);
- const path = getHomeRoomPath(roomIdOrAlias, eventId);
- navigate(
- viaServers
- ? withSearchParam<_RoomSearchParams>(path, {
- viaServers: encodeSearchParamValueArray(viaServers),
- })
- : path
- );
- }}
- />
- )}
- >
- )}
-
-
- );
-}
-
-/** A short message that wraps inside the navigator; never a raw request error. */
-function EstateError({ message }: { message: string }) {
- return (
-
- {message}
-
- );
-}
-
-function EstateTiers({ rootId }: { rootId: string }) {
- const { fetching, error, tiers } = useEstate(rootId);
-
- return (
- <>
- {tiers.map((tier) => (
-
- ))}
- {fetching && (
-
-
-
- )}
- {error && (
-
- )}
- >
- );
-}
-
-/** The single navigator: every nav slot that lists rooms mounts this. */
-export function EstateNavigator() {
- const openRoomFinder = useSetAtom(searchModalAtom);
- const { rootId, error } = useEstateRootId();
- const scrollRef = useRef(null);
- const mx = useMatrixClient();
- const title = (rootId && mx.getRoom(rootId)?.name) || 'GameCult';
-
- return (
-
-
-
-
-
- {title}
-
-
- openRoomFinder(true)}
- >
-
-
-
-
-
-
-
- {rootId && }
- {!rootId && error && (
-
- )}
- {!rootId && !error && (
-
-
-
- )}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/features/estate/buildEstate.test.ts b/src/app/features/estate/buildEstate.test.ts
deleted file mode 100644
index 8f898a0f..00000000
--- a/src/app/features/estate/buildEstate.test.ts
+++ /dev/null
@@ -1,326 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import { buildEstate, EstateHierarchyRoom, EstateInput, stewardOfChild } from './buildEstate';
-
-const ROOT = '!root:gamecult.org';
-const PROJECTS = '!projects:gamecult.org';
-
-/** A Space whose child entries may carry the steward Mind, as the accounts script writes it. */
-const space = (
- id: string,
- name: string,
- children: string[],
- stewards: Record = {}
-): EstateHierarchyRoom => ({
- room_id: id,
- name,
- room_type: 'm.space',
- children_state: children.map((c) => ({
- state_key: c,
- content: {
- via: ['gamecult.org'],
- ...(stewards[c] ? { 'org.gamecult.steward': { mind: stewards[c], repo: 'x' } } : {}),
- },
- })),
-});
-const room = (id: string, name: string, alias?: string): EstateHierarchyRoom => ({
- room_id: id,
- name,
- canonical_alias: alias,
-});
-
-const hierarchy = [
- space(ROOT, 'GameCult', ['!general:gamecult.org', PROJECTS]),
- room('!general:gamecult.org', 'General', '#general:gamecult.org'),
- space(
- PROJECTS,
- 'Projects',
- ['!cultlib:gamecult.org', '!aeth:gamecult.org', '!misc:gamecult.org'],
- { '!cultlib:gamecult.org': 'eureka', '!aeth:gamecult.org': 'void' }
- ),
- room('!cultlib:gamecult.org', 'CultLib', '#cultlib:gamecult.org'),
- room('!aeth:gamecult.org', 'Aetheria', '#Aetheria:gamecult.org'),
- room('!misc:gamecult.org', 'Misc', '#misc:gamecult.org'),
-];
-
-const input = (over: Partial = {}): EstateInput => ({
- rootId: ROOT,
- hierarchy,
- membership: new Map(),
- directRoomIds: [],
- ...over,
-});
-
-const find = (tiers: ReturnType, roomId: string) =>
- tiers
- .flatMap((t) => [...t.loose, ...t.groups.flatMap((g) => g.rooms)])
- .find((r) => r.roomId === roomId);
-
-describe('buildEstate', () => {
- it('lists an invited project room as invited', () => {
- const tiers = buildEstate(
- input({ membership: new Map([['!cultlib:gamecult.org', 'invite']]) })
- );
- expect(find(tiers, '!cultlib:gamecult.org')?.state).toBe('invited');
- });
-
- it('lists a hierarchy room the user has not joined as joinable, beside joined ones', () => {
- const tiers = buildEstate(input({ membership: new Map([['!general:gamecult.org', 'join']]) }));
- expect(find(tiers, '!general:gamecult.org')?.state).toBe('joined');
- expect(find(tiers, '!aeth:gamecult.org')?.state).toBe('joinable');
- expect(find(tiers, '!aeth:gamecult.org')?.via).toEqual(['gamecult.org']);
- });
-
- it('omits a room absent from the hierarchy even when the client store knows it', () => {
- const hidden = '!estate-secret:gamecult.org';
- const tiers = buildEstate(
- input({
- membership: new Map([[hidden, 'join']]),
- // The root names the room as a child, but the server did not return it.
- hierarchy: [
- space(ROOT, 'GameCult', ['!general:gamecult.org', PROJECTS, hidden]),
- ...hierarchy.slice(1),
- ],
- })
- );
- expect(find(tiers, hidden)).toBeUndefined();
- });
-
- it('lists every room the server returned, whatever the user may join', () => {
- const tiers = buildEstate(input());
- expect(tiers.flatMap((t) => [...t.loose, ...t.groups.flatMap((g) => g.rooms)])).toHaveLength(4);
- });
-
- it('groups rooms by steward Mind from the steward on each Space child entry', () => {
- const projects = buildEstate(input()).find((t) => t.id === PROJECTS)!;
- expect(projects.groups.map((g) => [g.mind, g.rooms.map((r) => r.roomId)])).toEqual([
- ['eureka', ['!cultlib:gamecult.org']],
- ['void', ['!aeth:gamecult.org']],
- ]);
- });
-
- it('sorts steward groups by display label', () => {
- const projects = buildEstate(
- input({
- mindLabels: new Map([
- ['eureka', 'Zed'],
- ['void', 'Alpha'],
- ]),
- })
- ).find((t) => t.id === PROJECTS)!;
- expect(projects.groups.map((g) => g.label)).toEqual(['Alpha', 'Zed']);
- });
-
- it('lists a room with no alias match loose in its tier', () => {
- const projects = buildEstate(input()).find((t) => t.id === PROJECTS)!;
- expect(projects.loose.map((r) => r.roomId)).toEqual(['!misc:gamecult.org']);
- });
-
- it('groups an invited-only user by the same child-entry stewards, with no root state', () => {
- const invitee = new Map([
- [ROOT, 'invite'],
- [PROJECTS, 'invite'],
- ['!cultlib:gamecult.org', 'invite'],
- ['!aeth:gamecult.org', 'invite'],
- ]);
- const projects = buildEstate(input({ membership: invitee })).find((t) => t.id === PROJECTS)!;
- expect(projects.groups.map((g) => [g.mind, g.rooms.map((r) => r.state)])).toEqual([
- ['eureka', ['invited']],
- ['void', ['invited']],
- ]);
- });
-
- it('ignores a steward that is not a non-empty string', () => {
- const odd = (steward: unknown): EstateHierarchyRoom[] => [
- {
- ...space(ROOT, 'GameCult', ['!a:gamecult.org']),
- children_state: [
- { state_key: '!a:gamecult.org', content: { 'org.gamecult.steward': { mind: steward } } },
- ],
- },
- room('!a:gamecult.org', 'A'),
- ];
- [42, '', undefined, null].forEach((m) => {
- const t = buildEstate(input({ hierarchy: odd(m) }))[0];
- expect(t.groups).toEqual([]);
- expect(t.loose).toHaveLength(1);
- });
- });
-
- it('makes one tier per sub-Space in hierarchy order, root rooms first, Direct last', () => {
- const second = '!second:gamecult.org';
- const tiers = buildEstate(
- input({
- hierarchy: [
- space(ROOT, 'GameCult', ['!general:gamecult.org', PROJECTS, second]),
- ...hierarchy.slice(1),
- space(second, 'Workshop', ['!w1:gamecult.org']),
- room('!w1:gamecult.org', 'Bench'),
- ],
- directRoomIds: ['!dm:gamecult.org'],
- membership: new Map([['!dm:gamecult.org', 'join']]),
- })
- );
- expect(tiers.map((t) => t.label)).toEqual([
- 'GameCult',
- 'Projects',
- 'Workshop',
- 'Direct Messages',
- ]);
- expect(tiers[3].loose[0].direct).toBe(true);
- expect(tiers[0].loose.map((r) => r.name)).toEqual(['General']);
- expect(tiers[3].loose[0]).toMatchObject({ roomId: '!dm:gamecult.org', parentId: null });
- });
-
- it('takes join servers from the matching children_state entry', () => {
- const tiers = buildEstate(
- input({
- hierarchy: [
- {
- ...space(ROOT, 'GameCult', ['!a:gamecult.org', '!b:gamecult.org']),
- children_state: [
- { state_key: '!a:gamecult.org', content: { via: ['one.example'] } },
- { state_key: '!b:gamecult.org', content: { via: ['two.example'] } },
- ],
- },
- room('!a:gamecult.org', 'A'),
- room('!b:gamecult.org', 'B'),
- ],
- })
- );
- expect(find(tiers, '!a:gamecult.org')?.via).toEqual(['one.example']);
- expect(find(tiers, '!b:gamecult.org')?.via).toEqual(['two.example']);
- });
-
- it('puts every room of one Mind in a single group', () => {
- const tiers = buildEstate(
- input({
- hierarchy: [
- ...hierarchy.slice(0, 2),
- space(PROJECTS, 'Projects', ['!cultlib:gamecult.org', '!aeth:gamecult.org'], {
- '!cultlib:gamecult.org': 'eureka',
- '!aeth:gamecult.org': 'eureka',
- }),
- ...hierarchy.slice(3, 5),
- ],
- })
- );
- const projects = tiers.find((t) => t.id === PROJECTS)!;
- expect(projects.groups).toHaveLength(1);
- expect(projects.groups[0].rooms.map((r) => r.roomId)).toEqual([
- '!cultlib:gamecult.org',
- '!aeth:gamecult.org',
- ]);
- });
-
- it('sorts group labels without regard to case', () => {
- const projects = buildEstate(
- input({
- mindLabels: new Map([
- ['eureka', 'beta'],
- ['void', 'Alpha'],
- ]),
- })
- ).find((t) => t.id === PROJECTS)!;
- expect(projects.groups.map((g) => g.label)).toEqual(['Alpha', 'beta']);
- });
-
- it('omits a tier whose Space returned no rooms and the Direct tier when there are no DMs', () => {
- const tiers = buildEstate(
- input({
- hierarchy: [
- space(ROOT, 'GameCult', ['!general:gamecult.org', PROJECTS]),
- room('!general:gamecult.org', 'General'),
- space(PROJECTS, 'Projects', ['!hidden:gamecult.org']),
- ],
- })
- );
- expect(tiers.map((t) => t.id)).toEqual([ROOT]);
- });
-
- it('puts direct messages in the final tier, off any Space', () => {
- const tiers = buildEstate(input({ directRoomIds: ['!dm:gamecult.org'] }));
- expect(tiers[tiers.length - 1]).toMatchObject({ id: 'direct', groups: [] });
- });
-
- it('is identical for identical inputs', () => {
- expect(buildEstate(input())).toEqual(buildEstate(input()));
- });
-});
-
-describe('stewardOfChild', () => {
- it('reads the Mind from the org.gamecult.steward key of the child content', () => {
- expect(stewardOfChild({ via: ['x'], 'org.gamecult.steward': { mind: 'void' } })).toBe('void');
- });
-
- it('has no steward without the key, content or a string Mind', () => {
- expect(stewardOfChild(undefined)).toBeUndefined();
- expect(stewardOfChild({ via: ['x'] })).toBeUndefined();
- expect(stewardOfChild({ 'org.gamecult.steward': {} })).toBeUndefined();
- });
-});
-
-describe('Elsewhere', () => {
- const strays = (over: Partial) =>
- buildEstate(input({ membership: new Map([['!adhoc:x', 'join']]), ...over })).find(
- (t) => t.id === 'elsewhere'
- );
-
- it('lists a joined non-DM room the hierarchy does not return, named from the store', () => {
- const t = strays({
- strayRoomIds: ['!adhoc:x'],
- roomNames: new Map([['!adhoc:x', 'Meeting']]),
- })!;
- expect(t.label).toBe('Elsewhere');
- expect(t.loose).toEqual([
- {
- roomId: '!adhoc:x',
- name: 'Meeting',
- state: 'joined',
- via: [],
- parentId: null,
- direct: false,
- },
- ]);
- });
-
- it('does not repeat a room the hierarchy lists or a DM, and is last before Direct', () => {
- const tiers = buildEstate(
- input({
- membership: new Map([
- ['!general:gamecult.org', 'join'],
- ['!dm:x', 'join'],
- ['!adhoc:x', 'join'],
- ]),
- strayRoomIds: ['!general:gamecult.org', '!dm:x', '!adhoc:x'],
- directRoomIds: ['!dm:x'],
- })
- );
- expect(tiers.map((t) => t.id)).toEqual([ROOT, PROJECTS, 'elsewhere', 'direct']);
- expect(tiers[2].loose.map((r) => r.roomId)).toEqual(['!adhoc:x']);
- });
-
- it('never lists a room the user has not joined', () => {
- expect(
- strays({ membership: new Map([['!adhoc:x', 'invite']]), strayRoomIds: ['!adhoc:x'] })
- ).toBeUndefined();
- });
-
- it('is absent when there are no strays', () => {
- expect(strays({})).toBeUndefined();
- });
-});
-
-describe('direct invites', () => {
- it('lists an invited DM in the Direct tier as invited, named from the store', () => {
- const tiers = buildEstate(
- input({
- membership: new Map([['!dm:x', 'invite']]),
- directRoomIds: ['!dm:x'],
- roomNames: new Map([['!dm:x', 'Void']]),
- })
- );
- expect(tiers[tiers.length - 1].loose).toEqual([
- { roomId: '!dm:x', name: 'Void', state: 'invited', via: [], parentId: null, direct: true },
- ]);
- });
-});
diff --git a/src/app/features/estate/buildEstate.ts b/src/app/features/estate/buildEstate.ts
deleted file mode 100644
index b9232beb..00000000
--- a/src/app/features/estate/buildEstate.ts
+++ /dev/null
@@ -1,186 +0,0 @@
-/**
- * The estate index: the only code that decides which rooms the navigator lists,
- * in which tier and steward group, and whether each is joined, invited or joinable.
- *
- * The server's /hierarchy of the root Space is the access rule. A room the
- * hierarchy does not return is never listed, whatever the client store knows;
- * a room it returns is never hidden by the client. The one licensed addition is
- * the user's own joined rooms that sit outside the Space (the Elsewhere tier).
- *
- * A room's steward is read from the hierarchy too: the content of the
- * m.space.child entry that lists the room carries org.gamecult.steward, which an
- * invited-only member can read (she cannot read the Space's state).
- */
-
-export const STEWARD_KEY = 'org.gamecult.steward';
-
-export type SpaceChildContent = {
- via?: string[];
- /** The Mind who stewards the room, as the accounts script writes it. */
- [STEWARD_KEY]?: { mind?: unknown };
-};
-
-export type EstateHierarchyRoom = {
- room_id: string;
- name?: string;
- canonical_alias?: string;
- room_type?: string;
- children_state?: { state_key: string; content?: SpaceChildContent }[];
-};
-
-export type EstateMembership = 'joined' | 'invited' | 'joinable';
-
-export type EstateRoom = {
- roomId: string;
- name: string;
- state: EstateMembership;
- /** Servers to join through, from the hierarchy's children_state. */
- via: string[];
- /** The Space tier the room sits in; null for direct messages and Elsewhere rooms. */
- parentId: string | null;
- direct: boolean;
-};
-
-export type EstateGroup = {
- mind: string;
- label: string;
- rooms: EstateRoom[];
-};
-
-export type EstateTier = {
- id: string;
- label: string;
- /** Rooms without a steward. */
- loose: EstateRoom[];
- groups: EstateGroup[];
-};
-
-export type EstateInput = {
- rootId: string;
- /** Rooms from the server's hierarchy of the root, in hierarchy order. */
- hierarchy: readonly EstateHierarchyRoom[];
- /** The user's own membership per room id, from the client store. */
- membership: ReadonlyMap;
- /** Joined and invited direct messages. */
- directRoomIds: readonly string[];
- /** Joined non-DM, non-Space rooms, whether or not the hierarchy lists them. */
- strayRoomIds?: readonly string[];
- /** Names for rooms the hierarchy does not name (direct and stray rooms). */
- roomNames?: ReadonlyMap;
- /** Display names for Minds, keyed by localpart. */
- mindLabels?: ReadonlyMap;
-};
-
-export const DIRECT_TIER_ID = 'direct';
-export const ELSEWHERE_TIER_ID = 'elsewhere';
-
-const SPACE_TYPE = 'm.space';
-
-/** The Mind a Space child entry names as the room's steward, if any. */
-export const stewardOfChild = (content: SpaceChildContent | undefined): string | undefined => {
- const mind = content?.[STEWARD_KEY]?.mind;
- return typeof mind === 'string' && mind !== '' ? mind : undefined;
-};
-
-const stateOf = (
- roomId: string,
- membership: ReadonlyMap
-): EstateMembership => {
- const m = membership.get(roomId);
- if (m === 'join') return 'joined';
- if (m === 'invite') return 'invited';
- return 'joinable';
-};
-
-const roomLabel = (r: EstateHierarchyRoom): string => r.name || r.canonical_alias || r.room_id;
-
-export const buildEstate = ({
- rootId,
- hierarchy,
- membership,
- directRoomIds,
- strayRoomIds = [],
- roomNames,
- mindLabels,
-}: EstateInput): EstateTier[] => {
- const direct = new Set(directRoomIds);
- const byId = new Map(hierarchy.map((r) => [r.room_id, r]));
- const spaces = hierarchy.filter((r) => r.room_type === SPACE_TYPE);
- const root = byId.get(rootId);
-
- // Root first, then every sub-Space in hierarchy order.
- const tierSpaces = root ? [root, ...spaces.filter((s) => s.room_id !== rootId)] : spaces;
-
- const tiers: EstateTier[] = tierSpaces.map((space) => {
- const childIds = new Set((space.children_state ?? []).map((c) => c.state_key));
- const tier: EstateTier = { id: space.room_id, label: roomLabel(space), loose: [], groups: [] };
- const groups = new Map();
-
- hierarchy.forEach((r) => {
- if (r.room_type === SPACE_TYPE || !childIds.has(r.room_id) || direct.has(r.room_id)) return;
- const content = space.children_state?.find((c) => c.state_key === r.room_id)?.content;
- const room: EstateRoom = {
- roomId: r.room_id,
- name: roomLabel(r),
- state: stateOf(r.room_id, membership),
- via: content?.via ?? [],
- parentId: space.room_id,
- direct: false,
- };
- const mind = stewardOfChild(content);
- if (!mind) {
- tier.loose.push(room);
- return;
- }
- let group = groups.get(mind);
- if (!group) {
- group = { mind, label: mindLabels?.get(mind) ?? mind, rooms: [] };
- groups.set(mind, group);
- }
- group.rooms.push(room);
- });
-
- tier.groups = [...groups.values()].sort((a, b) =>
- a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })
- );
- return tier;
- });
-
- const directTier: EstateTier = {
- id: DIRECT_TIER_ID,
- label: 'Direct Messages',
- loose: directRoomIds.map((roomId) => ({
- roomId,
- name: roomNames?.get(roomId) ?? roomId,
- state: stateOf(roomId, membership),
- via: [],
- parentId: null,
- direct: true,
- })),
- groups: [],
- };
-
- const listedIds = new Set(hierarchy.map((r) => r.room_id));
- const elsewhereTier: EstateTier = {
- id: ELSEWHERE_TIER_ID,
- label: 'Elsewhere',
- loose: strayRoomIds
- .filter((id) => !listedIds.has(id) && !direct.has(id) && membership.get(id) === 'join')
- .map((roomId) => ({
- roomId,
- name: roomNames?.get(roomId) ?? roomId,
- state: 'joined' as const,
- via: [],
- parentId: null,
- direct: false,
- })),
- groups: [],
- };
-
- const listed = tiers.filter((t) => t.loose.length > 0 || t.groups.length > 0);
- return [
- ...listed,
- ...(elsewhereTier.loose.length > 0 ? [elsewhereTier] : []),
- ...(directTier.loose.length > 0 ? [directTier] : []),
- ];
-};
diff --git a/src/app/features/estate/config.test.ts b/src/app/features/estate/config.test.ts
deleted file mode 100644
index 4304e1c2..00000000
--- a/src/app/features/estate/config.test.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { readFileSync } from 'fs';
-import { describe, expect, it } from 'vitest';
-
-describe('repo config.json', () => {
- it('names the GameCult Space first, so a wholesale deploy resolves the estate root', () => {
- const config = JSON.parse(readFileSync('config.json', 'utf8'));
- expect(config.featuredCommunities.spaces[0]).toBe('#gamecult:gamecult.org');
- });
-});
diff --git a/src/app/features/estate/index.ts b/src/app/features/estate/index.ts
deleted file mode 100644
index 761586c4..00000000
--- a/src/app/features/estate/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './EstateNavigator';
diff --git a/src/app/features/estate/joinEstateRoom.test.ts b/src/app/features/estate/joinEstateRoom.test.ts
deleted file mode 100644
index 932e7cce..00000000
--- a/src/app/features/estate/joinEstateRoom.test.ts
+++ /dev/null
@@ -1,352 +0,0 @@
-import { describe, expect, it, vi } from 'vitest';
-import {
- activateEstateRoom,
- awaitMyJoin,
- joinEstateRoomIdOrAlias,
- joinEstateRoomOrThrow,
- runJoinCommand,
-} from './joinEstateRoom';
-
-const room = (state: 'joined' | 'invited' | 'joinable') => ({
- roomId: '!r:gamecult.org',
- state,
- via: ['gamecult.org'],
-});
-
-/** A client that records every call; anything but joinRoom is a peek or a side door. */
-const recordingClient = (joinRoom = vi.fn().mockResolvedValue({ roomId: '!r:gamecult.org' })) => {
- const calls: string[] = [];
- const mx = new Proxy({} as Record, {
- get: (_t, prop: string) => {
- if (prop === 'joinRoom') {
- return (...args: unknown[]) => {
- calls.push('joinRoom');
- return joinRoom(...args);
- };
- }
- if (prop === 'getRoom') return () => null;
- if (prop === 'getSafeUserId') return () => '@me:test';
- return () => {
- calls.push(prop);
- throw new Error(`unexpected client call: ${prop}`);
- };
- },
- });
- return { mx: mx as never, calls, joinRoom };
-};
-
-describe('activateEstateRoom', () => {
- it('opens a joined room without touching the client', async () => {
- const { mx, calls } = recordingClient();
- expect(await activateEstateRoom(mx, room('joined'))).toEqual({
- kind: 'open',
- roomId: '!r:gamecult.org',
- });
- expect(calls).toEqual([]);
- });
-
- it.each(['invited', 'joinable'] as const)(
- 'joins a %s room on the tap, through the hierarchy servers, and nothing else',
- async (state) => {
- const { mx, calls, joinRoom } = recordingClient();
- const result = await activateEstateRoom(mx, room(state));
- expect(result).toEqual({ kind: 'joined', roomId: '!r:gamecult.org' });
- expect(joinRoom).toHaveBeenCalledWith('!r:gamecult.org', { viaServers: ['gamecult.org'] });
- expect(calls).toEqual(['joinRoom']); // no peekInRoom, no read, no second path
- }
- );
-
- it('falls back to the error message, then to a generic one', async () => {
- const named = recordingClient(
- vi
- .fn()
- .mockRejectedValue(
- Object.assign(new Error('Boom'), { httpStatus: 403, errcode: 'M_FORBIDDEN' })
- )
- );
- expect(await activateEstateRoom(named.mx, room('joinable'))).toEqual({
- kind: 'refused',
- message: 'Boom',
- });
- const bare = recordingClient(
- vi.fn().mockRejectedValue({ httpStatus: 403, errcode: 'M_FORBIDDEN' })
- );
- expect(await activateEstateRoom(bare.mx, room('joinable'))).toEqual({
- kind: 'refused',
- message: 'Could not join',
- });
- });
-
- it('reports the server refusal and does not decide access itself', async () => {
- const refusal = Object.assign(new Error('M_FORBIDDEN'), {
- httpStatus: 403,
- errcode: 'M_FORBIDDEN',
- data: { error: 'You are not invited to this room.' },
- });
- const { mx, joinRoom } = recordingClient(vi.fn().mockRejectedValue(refusal));
- const result = await activateEstateRoom(mx, room('joinable'));
- expect(result).toEqual({ kind: 'refused', message: 'You are not invited to this room.' });
- expect(joinRoom).toHaveBeenCalledTimes(1);
- });
-});
-
-/** A client holding DM invites as the server sends them: the mark is in the invite event only. */
-const dmClient = (opts: {
- status?: number;
- errcode?: string;
- refuse?: boolean;
- drop?: boolean;
- failWrite?: boolean;
- invites: Record;
-}) => {
- let direct: Record | undefined;
- const writes: Record[] = [];
- const joined: string[] = [];
- const joinOpts: unknown[] = [];
- const mx = {
- getSafeUserId: () => '@me:test',
- getRoom: (roomId: string) => ({
- getMyMembership: () => 'invite',
- getMember: () => ({
- events: {
- member: {
- getContent: () => ({ membership: 'invite', is_direct: true }),
- getSender: () => opts.invites[roomId],
- },
- },
- }),
- }),
- getAccountData: () => (direct ? { getContent: () => direct } : undefined),
- setAccountData: async (_t: string, content: Record) => {
- if (opts.failWrite) throw new Error('account data write failed');
- await new Promise((r) => {
- setTimeout(r, 5);
- });
- writes.push(structuredClone(content));
- direct = structuredClone(content);
- },
- joinRoom: async (roomId: string, joinArgs?: unknown) => {
- joinOpts.push(joinArgs);
- // the join lands only after m.direct has been written: record what it held
- joined.push(JSON.stringify(direct ?? {}));
- if (opts.refuse)
- throw Object.assign(new Error('M_FORBIDDEN'), { httpStatus: 403, errcode: 'M_FORBIDDEN' });
- if (opts.status)
- throw Object.assign(new Error('Gateway Time-out'), {
- httpStatus: opts.status,
- errcode: opts.errcode,
- });
- if (opts.drop) throw new Error('fetch failed');
- return { roomId };
- },
- };
- return { mx: mx as never, writes, joined, joinOpts, current: () => direct };
-};
-const tap = (id: string) => ({ roomId: id, state: 'invited' as const, via: ['test'] });
-
-describe('activateEstateRoom for a direct message invite', () => {
- it('lists the room under the inviter before the join is sent', async () => {
- const c = dmClient({ invites: { '!dm1:test': '@friend:test' } });
- await activateEstateRoom(c.mx, tap('!dm1:test'));
- expect(c.joined).toEqual([JSON.stringify({ '@friend:test': ['!dm1:test'] })]);
- });
-
- it('keeps both rooms when two DM joins are tapped together', async () => {
- const c = dmClient({ invites: { '!dm2:test': '@friend:test', '!dm3:test': '@friend:test' } });
- const results = await Promise.all([
- activateEstateRoom(c.mx, tap('!dm2:test')),
- activateEstateRoom(c.mx, tap('!dm3:test')),
- ]);
- expect(results.map((r) => r.kind)).toEqual(['joined', 'joined']);
- expect(c.current()).toEqual({ '@friend:test': ['!dm2:test', '!dm3:test'] });
- });
-
- it('takes the room out again when the join is refused', async () => {
- const c = dmClient({ refuse: true, invites: { '!dm1:test': '@friend:test' } });
- const result = await activateEstateRoom(c.mx, tap('!dm1:test'));
- expect(result).toEqual({ kind: 'refused', message: 'M_FORBIDDEN' });
- expect(c.current()).toEqual({ '@friend:test': [] });
- });
-});
-
-describe('activateEstateRoom when the join gets no answer', () => {
- it('keeps the room listed and shows a retryable message', async () => {
- const c = dmClient({ drop: true, invites: { '!dm1:test': '@friend:test' } });
- const result = await activateEstateRoom(c.mx, tap('!dm1:test'));
- expect(result.kind).toBe('refused');
- expect((result as { message: string }).message).toMatch(/tap again to retry/);
- expect(c.current()).toEqual({ '@friend:test': ['!dm1:test'] });
- });
-});
-
-describe('activateEstateRoom when the filing cannot be written', () => {
- it('reports the failure and sends no join', async () => {
- const c = dmClient({ failWrite: true, invites: { '!dm1:test': '@friend:test' } });
- const result = await activateEstateRoom(c.mx, tap('!dm1:test'));
- expect(result).toEqual({ kind: 'refused', message: 'account data write failed' });
- expect(c.joined).toEqual([]);
- });
-});
-
-describe('activateEstateRoom when a room that is not a direct message refuses', () => {
- it('reports the refusal and leaves m.direct alone', async () => {
- const joinRoom = vi
- .fn()
- .mockRejectedValue(
- Object.assign(new Error('no'), { httpStatus: 403, errcode: 'M_FORBIDDEN' })
- );
- const { mx, calls } = recordingClient(joinRoom);
- const result = await activateEstateRoom(mx, room('joinable'));
- expect(result.kind).toBe('refused');
- expect(calls).toEqual(['joinRoom']);
- });
-});
-
-describe('activateEstateRoom refusal bounds', () => {
- it.each([400, 429, 499])('unfiles the room on a %i that carries an errcode', async (status) => {
- const c = dmClient({
- status,
- errcode: 'M_LIMIT_EXCEEDED',
- invites: { '!dm1:test': '@friend:test' },
- });
- const result = await activateEstateRoom(c.mx, tap('!dm1:test'));
- expect(result.kind).toBe('refused');
- expect((result as { message: string }).message).not.toMatch(/tap again to retry/);
- expect(c.current()).toEqual({ '@friend:test': [] });
- });
-
- it.each([399, 500])('keeps the room listed on a %i that carries an errcode', async (status) => {
- const c = dmClient({ status, errcode: 'M_UNKNOWN', invites: { '!dm1:test': '@friend:test' } });
- await activateEstateRoom(c.mx, tap('!dm1:test'));
- expect(c.current()).toEqual({ '@friend:test': ['!dm1:test'] });
- });
-});
-
-describe('activateEstateRoom when a gateway answers instead of the server', () => {
- it.each([502, 503, 504, 404])('keeps the room listed on a %i with no errcode', async (status) => {
- const c = dmClient({ status, invites: { '!dm1:test': '@friend:test' } });
- const result = await activateEstateRoom(c.mx, tap('!dm1:test'));
- expect(result.kind).toBe('refused');
- expect((result as { message: string }).message).toMatch(/tap again to retry/);
- expect(c.current()).toEqual({ '@friend:test': ['!dm1:test'] });
- });
-
- it('keeps the room listed on a 5xx even when it carries an errcode', async () => {
- const c = dmClient({
- status: 500,
- errcode: 'M_UNKNOWN',
- invites: { '!dm1:test': '@friend:test' },
- });
- await activateEstateRoom(c.mx, tap('!dm1:test'));
- expect(c.current()).toEqual({ '@friend:test': ['!dm1:test'] });
- });
-});
-
-describe('joinEstateRoomIdOrAlias, the room card and /join', () => {
- it('files a direct invite opened by its link before the join is sent', async () => {
- const c = dmClient({ invites: { '!dm1:test': '@friend:test' } });
- const result = await joinEstateRoomIdOrAlias(c.mx, '!dm1:test', ['test']);
- expect(result).toEqual({ kind: 'joined', roomId: '!dm1:test' });
- expect(c.joined).toEqual([JSON.stringify({ '@friend:test': ['!dm1:test'] })]);
- });
-
- it('hands the via servers to the join', async () => {
- const c = dmClient({ invites: { '!dm1:test': '@friend:test' } });
- await joinEstateRoomIdOrAlias(c.mx, '!dm1:test', ['other.org']);
- expect(c.joinOpts).toEqual([{ viaServers: ['other.org'] }]);
- });
-
- it('joins an alias or an unheld room without touching m.direct', async () => {
- const { mx, calls } = recordingClient();
- await joinEstateRoomIdOrAlias(mx, '#room:gamecult.org');
- expect(calls).toEqual(['joinRoom']);
- });
-});
-
-describe('joinEstateRoomOrThrow, the lobby, tombstone and predecessor buttons', () => {
- it('files a direct invite and answers the joined room id', async () => {
- const c = dmClient({ invites: { '!dm1:test': '@friend:test' } });
- await expect(joinEstateRoomOrThrow(c.mx, '!dm1:test', ['test'])).resolves.toBe('!dm1:test');
- expect(c.joined).toEqual([JSON.stringify({ '@friend:test': ['!dm1:test'] })]);
- });
-
- it('throws the refusal and unfiles the room', async () => {
- const c = dmClient({ refuse: true, invites: { '!dm1:test': '@friend:test' } });
- await expect(joinEstateRoomOrThrow(c.mx, '!dm1:test')).rejects.toThrow('M_FORBIDDEN');
- expect(c.current()).toEqual({ '@friend:test': [] });
- });
-});
-
-describe('runJoinCommand, /join', () => {
- it('reports each refusal and joins the others', async () => {
- const c = dmClient({ refuse: true, invites: { '!dm1:test': '@friend:test' } });
- const reported: string[] = [];
- await runJoinCommand(c.mx, '!dm1:test notanaddress', (m) => reported.push(m));
- expect(reported).toEqual(['M_FORBIDDEN']);
- expect(c.joined).toHaveLength(1);
- });
-
- it('reports nothing when the join is accepted', async () => {
- const c = dmClient({ invites: { '!dm1:test': '@friend:test' } });
- const reported: string[] = [];
- await runJoinCommand(c.mx, '!dm1:test', (m) => reported.push(m));
- expect(reported).toEqual([]);
- expect(c.joined).toHaveLength(1);
- });
-});
-
-describe('awaitMyJoin', () => {
- /** A store that gains the room when the test says so, emitting like the SDK. */
- const store = () => {
- let membership: string | null = null;
- const handlers = new Set<(room: { roomId: string }, m: string) => void>();
- return {
- mx: {
- getRoom: () => (membership ? { getMyMembership: () => membership! } : null),
- on: (e: string, h: (room: { roomId: string }, m: string) => void) => {
- if (e === 'Room.myMembership') handlers.add(h);
- },
- off: (e: string, h: (room: { roomId: string }, m: string) => void) => {
- if (e === 'Room.myMembership') handlers.delete(h);
- },
- },
- handlers,
- become: (roomId: string, m: string) => {
- membership = m;
- handlers.forEach((h) => h({ roomId }, m));
- },
- };
- };
-
- it('resolves at once when the store already holds the joined room', async () => {
- const s = store();
- s.become('!r:x', 'join');
- expect(await awaitMyJoin(s.mx, '!r:x', 1000)).toBe(true);
- });
-
- it('stays pending while the store still shows an invite, then resolves on join', async () => {
- const s = store();
- let settled = false;
- const p = awaitMyJoin(s.mx, '!r:x', 1000).then((v) => {
- settled = true;
- return v;
- });
- s.become('!r:x', 'invite');
- s.become('!other:x', 'join');
- await Promise.resolve();
- expect(settled).toBe(false);
- s.become('!r:x', 'join');
- expect(await p).toBe(true);
- expect(s.handlers.size).toBe(0);
- });
-
- it('gives up after the timeout and stops listening', async () => {
- vi.useFakeTimers();
- const s = store();
- const p = awaitMyJoin(s.mx, '!r:x', 500);
- await vi.advanceTimersByTimeAsync(500);
- expect(await p).toBe(false);
- expect(s.handlers.size).toBe(0);
- vi.useRealTimers();
- });
-});
diff --git a/src/app/features/estate/joinEstateRoom.ts b/src/app/features/estate/joinEstateRoom.ts
deleted file mode 100644
index 4e1629d0..00000000
--- a/src/app/features/estate/joinEstateRoom.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import { EstateRoom } from './buildEstate';
-import {
- addRoomIdToMDirect,
- isRoomAlias,
- isRoomId,
- removeRoomIdFromMDirect,
-} from '../../utils/matrix';
-import { splitWithSpace } from '../../utils/common';
-import { isDirectInvite } from '../../utils/room';
-
-export type EstateActivation =
- | { kind: 'open'; roomId: string }
- | { kind: 'joined'; roomId: string }
- | { kind: 'refused'; message: string };
-
-const LOST_JOIN_RESPONSE =
- 'The server did not answer the join. It may have gone through; tap again to retry.';
-
-type JoinClient = Parameters[0];
-
-/**
- * What a tap on an estate row does. A joined room opens; any other room is
- * joined, and only here (plus a first post in the room view). Opening a room
- * never joins it and the client has no peek path. The server decides whether
- * the join is allowed; its refusal is reported, the row is not hidden.
- *
- * An invite marked is_direct carries the mark in the invite event the client
- * already holds, and the join event does not repeat it. So the room is listed
- * in m.direct under the inviter before the join is sent, and taken out again if
- * the join is refused by a Matrix error answer (a 4xx carrying an errcode). A
- * join that fails any other way (lost response, dropped connection, a gateway's
- * 5xx or a status with no errcode) may still have been accepted upstream, so the
- * entry stays and the tap is retryable. Nothing needs to recognise the DM after it
- * arrives. Every client join goes through here: the estate tap, the inbox Accept,
- * the room card, /join, the lobby and space Join buttons, the tombstone and
- * predecessor links. One owner for joining.
- */
-export const activateEstateRoom = async (
- mx: JoinClient,
- room: Pick & { via?: string[] }
-): Promise => {
- if (room.state === 'joined') return { kind: 'open', roomId: room.roomId };
- const refusal = (e: unknown): EstateActivation => {
- const err = e as { data?: { error?: string }; message?: string };
- return { kind: 'refused', message: err.data?.error ?? err.message ?? 'Could not join' };
- };
- const refusedByServer = (e: unknown) => {
- const err = e as { httpStatus?: unknown; errcode?: unknown; data?: { errcode?: unknown } };
- const status = err.httpStatus;
- const errcode = err.errcode ?? err.data?.errcode;
- return (
- typeof status === 'number' && status >= 400 && status < 500 && typeof errcode === 'string'
- );
- };
- const invite = room.state === 'invited' ? mx.getRoom(room.roomId) : null;
- const inviter = isDirectInvite(invite, mx.getSafeUserId())
- ? invite?.getMember(mx.getSafeUserId())?.events?.member?.getSender()
- : undefined;
- try {
- if (inviter) await addRoomIdToMDirect(mx, room.roomId, inviter);
- } catch (e) {
- return refusal(e);
- }
- try {
- const joined = await mx.joinRoom(room.roomId, { viaServers: room.via });
- return { kind: 'joined', roomId: joined.roomId };
- } catch (e) {
- if (!refusedByServer(e)) return { kind: 'refused', message: LOST_JOIN_RESPONSE };
- if (inviter) await removeRoomIdFromMDirect(mx, room.roomId).catch(() => undefined);
- return refusal(e);
- }
-};
-
-/**
- * activateEstateRoom for a caller that holds only an id or alias (room card,
- * /join): a room the client has an invite for is an invited room.
- */
-export const joinEstateRoomIdOrAlias = (
- mx: JoinClient,
- roomIdOrAlias: string,
- via?: string[]
-): Promise =>
- activateEstateRoom(mx, {
- roomId: roomIdOrAlias,
- state: mx.getRoom(roomIdOrAlias)?.getMyMembership() === 'invite' ? 'invited' : 'joinable',
- via,
- });
-
-/**
- * joinEstateRoomIdOrAlias for a caller that reports failure by throwing (a
- * useAsyncCallback button): the joined room id, or an Error carrying the refusal.
- */
-export const joinEstateRoomOrThrow = async (
- mx: JoinClient,
- roomIdOrAlias: string,
- via?: string[]
-): Promise => {
- const result = await joinEstateRoomIdOrAlias(mx, roomIdOrAlias, via);
- if (result.kind === 'refused') throw new Error(result.message);
- return result.roomId;
-};
-
-/**
- * The /join command: every room id or alias in the payload is joined, and each
- * refusal is handed to report, never dropped.
- */
-export const runJoinCommand = async (
- mx: JoinClient,
- payload: string,
- report: (message: string) => void
-): Promise => {
- const addresses = splitWithSpace(payload).filter((a) => isRoomId(a) || isRoomAlias(a));
- const results = await Promise.all(addresses.map((a) => joinEstateRoomIdOrAlias(mx, a)));
- results.forEach((r) => {
- if (r.kind === 'refused') report(r.message);
- });
-};
-
-type MembershipSource = {
- getRoom(roomId: string): { getMyMembership(): string } | null;
- on(event: 'Room.myMembership', handler: (room: { roomId: string }, m: string) => void): unknown;
- off(event: 'Room.myMembership', handler: (room: { roomId: string }, m: string) => void): unknown;
-};
-
-/**
- * joinRoom resolves when the server accepts the join, before the client store
- * holds the room; navigating then lands on the invite card. Resolves true once
- * the store says the user's membership is join, false after timeoutMs.
- */
-export const awaitMyJoin = (
- mx: MembershipSource,
- roomId: string,
- timeoutMs: number
-): Promise =>
- new Promise((resolve) => {
- if (mx.getRoom(roomId)?.getMyMembership() === 'join') {
- resolve(true);
- return;
- }
- let finish: (joined: boolean) => void = () => undefined;
- const onMembership = (room: { roomId: string }, membership: string) => {
- if (room.roomId === roomId && membership === 'join') finish(true);
- };
- const timer = setTimeout(() => finish(false), timeoutMs);
- finish = (joined) => {
- clearTimeout(timer);
- mx.off('Room.myMembership', onMembership);
- resolve(joined);
- };
- mx.on('Room.myMembership', onMembership);
- });
diff --git a/src/app/features/estate/useEstate.ts b/src/app/features/estate/useEstate.ts
deleted file mode 100644
index d27d4710..00000000
--- a/src/app/features/estate/useEstate.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { useCallback, useEffect, useMemo } from 'react';
-import { useAtomValue } from 'jotai';
-import { useQuery, useQueryClient } from '@tanstack/react-query';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { useClientConfig } from '../../hooks/useClientConfig';
-import { useFetchSpaceHierarchyLevel } from '../../hooks/useSpaceHierarchy';
-import { isDirectInvite, isRoom, isUnsupportedRoom } from '../../utils/room';
-import { useStateEventCallback } from '../../hooks/useStateEventCallback';
-import { allRoomsAtom } from '../../state/room-list/roomList';
-import { allInvitesAtom } from '../../state/room-list/inviteList';
-import { mDirectAtom } from '../../state/mDirectList';
-import { StateEvent } from '../../../types/matrix/room';
-import { buildEstate, EstateHierarchyRoom, EstateTier, stewardOfChild } from './buildEstate';
-
-const HIERARCHY_DEPTH = 10;
-
-type AliasedRoom = {
- roomId: string;
- getCanonicalAlias(): string | null;
- getAltAliases(): string[];
-};
-
-const findLoadedRoom = (
- mx: ReturnType,
- alias: string
-): string | undefined =>
- (mx.getRooms() as AliasedRoom[]).find(
- (r) => r.getCanonicalAlias() === alias || r.getAltAliases().includes(alias)
- )?.roomId;
-
-/** The estate's root Space: featuredCommunities.spaces[0] from the client config. */
-export const useEstateRootId = (): { rootId?: string; error?: Error } => {
- const mx = useMatrixClient();
- const alias = useClientConfig().featuredCommunities?.spaces?.[0];
- const loaded = alias && !alias.startsWith('#') ? alias : alias && findLoadedRoom(mx, alias);
- const resolved = useQuery({
- queryKey: ['estate-root', alias],
- queryFn: async () => (await mx.getRoomIdForAlias(alias!)).room_id,
- enabled: !!alias && !loaded,
- });
- if (!alias) return { error: new Error('No estate root Space is configured') };
- return { rootId: loaded || resolved.data, error: resolved.error ?? undefined };
-};
-
-export type Estate = {
- fetching: boolean;
- error: Error | null;
- tiers: EstateTier[];
-};
-
-/**
- * Feeds buildEstate: the server hierarchy of the root Space (which also carries
- * each room's steward), the user's membership, m.direct and direct invites, and
- * her joined rooms outside the Space. Recomputes on membership and Space child changes.
- */
-export const useEstate = (rootId: string): Estate => {
- const mx = useMatrixClient();
- const queryClient = useQueryClient();
- const { fetching, error, rooms } = useFetchSpaceHierarchyLevel(rootId, true, HIERARCHY_DEPTH);
- const joined = useAtomValue(allRoomsAtom);
- const invited = useAtomValue(allInvitesAtom);
- const mDirects = useAtomValue(mDirectAtom);
-
- const refetchHierarchy = useCallback(
- () => queryClient.invalidateQueries({ queryKey: [rootId, 'hierarchy_level'] }),
- [queryClient, rootId]
- );
- useStateEventCallback(
- mx,
- useCallback(
- (event) => {
- if (event.getType() === StateEvent.SpaceChild) refetchHierarchy();
- },
- [refetchHierarchy]
- )
- );
- // What the server lets this user see changes with invitations.
- useEffect(() => {
- refetchHierarchy();
- }, [invited.length, refetchHierarchy]);
-
- const tiers = useMemo(() => {
- const membership = new Map();
- invited.forEach((id) => membership.set(id, 'invite'));
- joined.forEach((id) => membership.set(id, 'join'));
- const hierarchy = Array.from(rooms.values()) as unknown as EstateHierarchyRoom[];
- const domain = mx.getDomain();
- const mindLabels = new Map();
- hierarchy.forEach((r) =>
- r.children_state?.forEach((c) => {
- const mind = stewardOfChild(c.content);
- const name = mind && mx.getUser(`@${mind}:${domain}`)?.displayName;
- if (mind && name) mindLabels.set(mind, name);
- })
- );
- const userId = mx.getUserId();
- const directInvites = invited.filter((id) => isDirectInvite(mx.getRoom(id), userId));
- const directRoomIds = [
- ...new Set([...Array.from(mDirects).filter((id) => joined.includes(id)), ...directInvites]),
- ];
- const strayRoomIds = joined.filter((id) => {
- const r = mx.getRoom(id);
- return !mDirects.has(id) && isRoom(r) && !isUnsupportedRoom(r);
- });
- const roomNames = new Map();
- [...directInvites, ...strayRoomIds].forEach((id) => {
- const name = mx.getRoom(id)?.name;
- if (name) roomNames.set(id, name);
- });
- return buildEstate({
- rootId,
- hierarchy,
- membership,
- directRoomIds,
- strayRoomIds,
- roomNames,
- mindLabels,
- });
- }, [mx, rootId, rooms, joined, invited, mDirects]);
-
- return { fetching, error, tiers };
-};
diff --git a/src/app/features/lobby/HierarchyItemMenu.tsx b/src/app/features/lobby/HierarchyItemMenu.tsx
index 1802e3b0..c6aff741 100644
--- a/src/app/features/lobby/HierarchyItemMenu.tsx
+++ b/src/app/features/lobby/HierarchyItemMenu.tsx
@@ -200,12 +200,16 @@ type HierarchyItemMenuProps = {
joined: boolean;
powerLevels?: IPowerLevels;
canEditChild: boolean;
+ pinned?: boolean;
+ onTogglePin?: (roomId: string) => void;
};
export function HierarchyItemMenu({
item,
joined,
powerLevels,
canEditChild,
+ pinned,
+ onTogglePin,
}: HierarchyItemMenuProps) {
const mx = useMatrixClient();
const [menuAnchor, setMenuAnchor] = useState();
@@ -260,6 +264,20 @@ export function HierarchyItemMenu({
{joined && (
+ {onTogglePin && (
+ {
+ onTogglePin(item.roomId);
+ handleRequestClose();
+ }}
+ >
+
+ {pinned ? 'Unpin from Sidebar' : 'Pin to Sidebar'}
+
+
+ )}
{
+ const sideSpaces = sidebarItems.flatMap((item) => {
+ if (typeof item === 'string') return item;
+ return item.content;
+ });
+
+ return new Set(sideSpaces);
+ }, [sidebarItems]);
+
const [spacesItems, setSpacesItem] = useState>(() => new Map());
useElementSizeObserver(
@@ -395,6 +415,18 @@ export function Lobby() {
navigate(getSpaceRoomPath(pSpaceIdOrAlias, getCanonicalAliasOrRoomId(mx, rId)));
};
+ const togglePinToSidebar = useCallback(
+ (rId: string) => {
+ const newItems = sidebarItemWithout(sidebarItems, rId);
+ if (!sidebarSpaces.has(rId)) {
+ newItems.push(rId);
+ }
+ const newSpacesContent = makeCinnySpacesContent(mx, newItems);
+ mx.setAccountData(AccountDataEvent.CinnySpaces as any, newSpacesContent as any);
+ },
+ [mx, sidebarItems, sidebarSpaces]
+ );
+
return (
@@ -467,6 +499,8 @@ export function Lobby() {
disabledReorder={reordering}
nextSpaceId={nextSpaceId}
getRoom={getRoom}
+ pinned={sidebarSpaces.has(item.space.roomId)}
+ togglePinToSidebar={togglePinToSidebar}
onSpacesFound={handleSpacesFound}
onOpenRoom={handleOpenRoom}
/>
diff --git a/src/app/features/lobby/LobbyHeader.tsx b/src/app/features/lobby/LobbyHeader.tsx
index ed526c68..a0c4d3ab 100644
--- a/src/app/features/lobby/LobbyHeader.tsx
+++ b/src/app/features/lobby/LobbyHeader.tsx
@@ -165,7 +165,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
{(onBack) => (
-
+
)}
@@ -218,11 +218,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
}
>
{(triggerRef) => (
- setPeopleDrawer((drawer) => !drawer)}
- >
+ setPeopleDrawer((drawer) => !drawer)}>
)}
@@ -239,12 +235,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
}
>
{(triggerRef) => (
-
+
)}
diff --git a/src/app/features/lobby/LobbyHero.css.tsx b/src/app/features/lobby/LobbyHero.css.tsx
index 3349213a..ad7e9382 100644
--- a/src/app/features/lobby/LobbyHero.css.tsx
+++ b/src/app/features/lobby/LobbyHero.css.tsx
@@ -6,7 +6,6 @@ export const LobbyHeroTopic = style({
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
- wordBreak: 'break-word',
':hover': {
cursor: 'pointer',
diff --git a/src/app/features/lobby/RoomItem.tsx b/src/app/features/lobby/RoomItem.tsx
index 132aaf8c..994cda05 100644
--- a/src/app/features/lobby/RoomItem.tsx
+++ b/src/app/features/lobby/RoomItem.tsx
@@ -19,7 +19,7 @@ import {
toRem,
} from 'folds';
import FocusTrap from 'focus-trap-react';
-import { JoinRule, Room } from 'matrix-js-sdk';
+import { JoinRule, MatrixError, Room } from 'matrix-js-sdk';
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import { SequenceCard } from '../../components/sequence-card';
@@ -38,7 +38,6 @@ import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room';
import { ItemDraggableTarget, useDraggableItem } from './DnD';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
-import { joinEstateRoomOrThrow } from '../estate/joinEstateRoom';
type RoomJoinButtonProps = {
roomId: string;
@@ -47,8 +46,8 @@ type RoomJoinButtonProps = {
function RoomJoinButton({ roomId, via }: RoomJoinButtonProps) {
const mx = useMatrixClient();
- const [joinState, join] = useAsyncCallback(
- useCallback(() => joinEstateRoomOrThrow(mx, roomId, via), [mx, roomId, via])
+ const [joinState, join] = useAsyncCallback(
+ useCallback(() => mx.joinRoom(roomId, { viaServers: via }), [mx, roomId, via])
);
const canJoin = joinState.status === AsyncStatus.Idle || joinState.status === AsyncStatus.Error;
@@ -61,8 +60,9 @@ function RoomJoinButton({ roomId, via }: RoomJoinButtonProps) {
- {joinState.error.message}
+ {joinState.error.data?.error || joinState.error.message}
+ {joinState.error.name}
}
@@ -75,7 +75,7 @@ function RoomJoinButton({ roomId, via }: RoomJoinButtonProps) {
size="400"
filled
tabIndex={0}
- aria-label={joinState.error.message}
+ aria-label={joinState.error.data?.error || joinState.error.message}
/>
)}
@@ -175,7 +175,6 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf
type RoomProfileProps = {
roomId: string;
- roomType?: string;
name: string;
topic?: string;
avatarUrl?: string;
@@ -186,7 +185,6 @@ type RoomProfileProps = {
};
function RoomProfile({
roomId,
- roomType,
name,
topic,
avatarUrl,
@@ -202,7 +200,9 @@ function RoomProfile({
roomId={roomId}
src={avatarUrl}
alt={name}
- renderFallback={() => }
+ renderFallback={() => (
+
+ )}
/>
@@ -338,7 +338,6 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
{(localSummary) => (
(
{summary && (
Room | undefined;
+ pinned: boolean;
+ togglePinToSidebar: (roomId: string) => void;
onSpacesFound: (spaceItems: IHierarchyRoom[]) => void;
onOpenRoom: MouseEventHandler;
};
@@ -56,6 +58,8 @@ export const SpaceHierarchy = forwardRef(
disabledReorder,
nextSpaceId,
getRoom,
+ pinned,
+ togglePinToSidebar,
onOpenRoom,
onSpacesFound,
},
@@ -132,6 +136,8 @@ export const SpaceHierarchy = forwardRef(
canEditChild={
!!parentPermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
}
+ pinned={pinned}
+ onTogglePin={togglePinToSidebar}
/>
)
}
diff --git a/src/app/features/lobby/SpaceItem.tsx b/src/app/features/lobby/SpaceItem.tsx
index e1ffbfe5..64a97900 100644
--- a/src/app/features/lobby/SpaceItem.tsx
+++ b/src/app/features/lobby/SpaceItem.tsx
@@ -18,7 +18,7 @@ import {
} from 'folds';
import FocusTrap from 'focus-trap-react';
import classNames from 'classnames';
-import { Room } from 'matrix-js-sdk';
+import { MatrixError, Room } from 'matrix-js-sdk';
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
import { useMatrixClient } from '../../hooks/useMatrixClient';
@@ -36,9 +36,6 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal';
import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal';
import { AddExistingModal } from '../add-existing';
-import { CreateRoomType } from '../../components/create-room/types';
-import { BetaNoticeBadge } from '../../components/BetaNoticeBadge';
-import { joinEstateRoomOrThrow } from '../estate/joinEstateRoom';
function SpaceProfileLoading() {
return (
@@ -113,8 +110,8 @@ function UnjoinedSpaceProfile({
}: UnjoinedSpaceProfileProps) {
const mx = useMatrixClient();
- const [joinState, join] = useAsyncCallback(
- useCallback(() => joinEstateRoomOrThrow(mx, roomId, via), [mx, roomId, via])
+ const [joinState, join] = useAsyncCallback(
+ useCallback(() => mx.joinRoom(roomId, { viaServers: via }), [mx, roomId, via])
);
const canJoin = joinState.status === AsyncStatus.Idle || joinState.status === AsyncStatus.Error;
@@ -252,8 +249,8 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
setCords(evt.currentTarget.getBoundingClientRect());
};
- const handleCreateRoom = (type?: CreateRoomType) => {
- openCreateRoomModal(item.roomId, type);
+ const handleCreateRoom = () => {
+ openCreateRoomModal(item.roomId);
setCords(undefined);
};
@@ -284,19 +281,9 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
radii="300"
variant="Primary"
fill="None"
- onClick={() => handleCreateRoom(CreateRoomType.TextRoom)}
+ onClick={handleCreateRoom}
>
- Chat Room
-
- handleCreateRoom(CreateRoomType.VoiceRoom)}
- after={ }
- >
- Voice Room
+ New Room
Existing Room
diff --git a/src/app/features/message-search/SearchFilters.tsx b/src/app/features/message-search/SearchFilters.tsx
index 6883e363..929dd1e9 100644
--- a/src/app/features/message-search/SearchFilters.tsx
+++ b/src/app/features/message-search/SearchFilters.tsx
@@ -29,7 +29,7 @@ import { SearchOrderBy } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { getRoomIconSrc } from '../../utils/room';
+import { joinRuleToIconSrc } from '../../utils/room';
import { factoryRoomIdByAtoZ } from '../../utils/sort';
import {
SearchItemStrGetter,
@@ -274,7 +274,9 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
before={
}
>
@@ -390,7 +392,10 @@ export function SearchFilters({
onClick={() => onSelectedRoomsChange(selectedRooms.filter((rId) => rId !== roomId))}
radii="Pill"
before={
-
+
}
after={ }
>
diff --git a/src/app/features/message-search/SearchResultGroup.tsx b/src/app/features/message-search/SearchResultGroup.tsx
index 14817cc4..62ef9c4b 100644
--- a/src/app/features/message-search/SearchResultGroup.tsx
+++ b/src/app/features/message-search/SearchResultGroup.tsx
@@ -203,12 +203,7 @@ export function SearchResultGroup({
src={getRoomAvatarUrl(mx, room, 96, useAuthentication)}
alt={room.name}
renderFallback={() => (
-
+
)}
/>
diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx
index 08341113..33b21bff 100644
--- a/src/app/features/room-nav/RoomNavItem.tsx
+++ b/src/app/features/room-nav/RoomNavItem.tsx
@@ -19,16 +19,15 @@ import {
} from 'folds';
import { useFocusWithin, useHover } from 'react-aria';
import FocusTrap from 'focus-trap-react';
-import { useAtom, useAtomValue } from 'jotai';
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
-import { getDirectRoomAvatarUrl, getRoomAvatarUrl, getStateEvent } from '../../utils/room';
+import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room';
import { nameInitials } from '../../utils/common';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomUnread } from '../../state/hooks/unread';
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
-import { getPowersLevelFromMatrixEvent, usePowerLevels } from '../../hooks/usePowerLevels';
+import { usePowerLevels } from '../../hooks/usePowerLevels';
import { copyToClipboard } from '../../utils/dom';
import { markAsRead } from '../../utils/notifications';
import { UseStateProvider } from '../../components/UseStateProvider';
@@ -49,18 +48,9 @@ import {
RoomNotificationMode,
} from '../../hooks/useRoomsNotificationPreferences';
import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
-import { getRoomCreatorsForRoomId, useRoomCreators } from '../../hooks/useRoomCreators';
-import { getRoomPermissionsAPI, useRoomPermissions } from '../../hooks/useRoomPermissions';
+import { useRoomCreators } from '../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../hooks/useRoomPermissions';
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';
-import { StateEvent } from '../../../types/matrix/room';
-import { webRTCSupported } from '../../utils/rtc';
type RoomNavItemMenuProps = {
room: Room;
@@ -219,24 +209,6 @@ const RoomNavItemMenu = forwardRef(
}
);
-function CallChatToggle() {
- const [chat, setChat] = useAtom(callChatAtom);
-
- return (
- setChat(!chat)}
- aria-pressed={chat}
- aria-label="Toggle Chat"
- variant="Background"
- fill="None"
- size="300"
- radii="300"
- >
-
-
- );
-}
-
type RoomNavItemProps = {
room: Room;
selected: boolean;
@@ -264,8 +236,6 @@ export function RoomNavItem({
(receipt) => receipt.userId !== mx.getUserId()
);
- const roomName = useRoomName(room);
-
const handleContextMenu: MouseEventHandler = (evt) => {
evt.preventDefault();
setMenuAnchor({
@@ -281,39 +251,6 @@ export function RoomNavItem({
};
const optionsVisible = hover || !!menuAnchor;
- const callSession = useCallSession(room);
- const callMembers = useCallMembers(callSession);
- const startCall = useCallStart(direct);
- const callEmbed = useCallEmbed();
- const callPref = useAtomValue(useCallPreferencesAtom());
- const autoDiscoveryInfo = useAutoDiscoveryInfo();
-
- const handleStartCall: MouseEventHandler = (evt) => {
- const powerLevelsEvent = getStateEvent(room, StateEvent.RoomPowerLevels);
- const powerLevels = getPowersLevelFromMatrixEvent(powerLevelsEvent);
- const creators = getRoomCreatorsForRoomId(mx, room.roomId);
- const permissions = getRoomPermissionsAPI(creators, powerLevels);
-
- const hasCallPermission = permissions.stateEvent(
- StateEvent.GroupCallMemberPrefix,
- mx.getSafeUserId()
- );
-
- // Do not join if missing permissions or no livekit support or no webRTC support
- if (!hasCallPermission || !livekitSupport(autoDiscoveryInfo) || !webRTCSupported()) {
- return;
- }
-
- // Do not join if already in call
- if (callEmbed) {
- return;
- }
- // Start call in second click
- if (selected) {
- evt.preventDefault();
- startCall(room, callPref);
- }
- };
return (
-
+
@@ -338,28 +275,25 @@ export function RoomNavItem({
? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
: getRoomAvatarUrl(mx, room, 96, useAuthentication)
}
- alt={roomName}
+ alt={room.name}
renderFallback={() => (
- {nameInitials(roomName)}
+ {nameInitials(room.name)}
)}
/>
) : (
)}
- {roomName}
+ {room.name}
{!optionsVisible && !unread && !selected && typingMember.length > 0 && (
@@ -373,30 +307,14 @@ export function RoomNavItem({
)}
{!optionsVisible && notificationMode !== RoomNotificationMode.Unset && (
-
- )}
- {callMembers.length > 0 && (
-
-
- {callMembers.length} Live
-
-
+
)}
{optionsVisible && (
- {selected && (callEmbed?.roomId === room.roomId || room.isCallRoom()) && (
-
- )}
- Advanced Options
+ Advance Options
diff --git a/src/app/features/room-settings/permissions/usePermissionItems.ts b/src/app/features/room-settings/permissions/usePermissionItems.ts
index d0500b8b..513f82b4 100644
--- a/src/app/features/room-settings/permissions/usePermissionItems.ts
+++ b/src/app/features/room-settings/permissions/usePermissionItems.ts
@@ -46,19 +46,6 @@ export const usePermissionGroups = (): PermissionGroup[] => {
],
};
- const callSettingsGroup: PermissionGroup = {
- name: 'Calls',
- items: [
- {
- location: {
- state: true,
- key: StateEvent.GroupCallMemberPrefix,
- },
- name: 'Start or Join Call',
- },
- ],
- };
-
const moderationGroup: PermissionGroup = {
name: 'Moderation',
items: [
@@ -190,13 +177,6 @@ export const usePermissionGroups = (): PermissionGroup[] => {
const otherSettingsGroup: PermissionGroup = {
name: 'Other',
items: [
- {
- location: {
- state: true,
- key: StateEvent.PoniesRoomEmotes,
- },
- name: 'Manage Emojis & Stickers',
- },
{
location: {
state: true,
@@ -216,7 +196,6 @@ export const usePermissionGroups = (): PermissionGroup[] => {
return [
messagesGroup,
- callSettingsGroup,
moderationGroup,
roomOverviewGroup,
roomSettingsGroup,
diff --git a/src/app/features/room/CallChatView.tsx b/src/app/features/room/CallChatView.tsx
deleted file mode 100644
index 6c32f4a0..00000000
--- a/src/app/features/room/CallChatView.tsx
+++ /dev/null
@@ -1,57 +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';
-
-export function CallChatView() {
- const { eventId } = useParams();
- const setChat = useSetAtom(callChatAtom);
- const screenSize = useScreenSizeContext();
-
- const handleClose = () => setChat(false);
-
- return (
-
-
-
-
-
- Chat
-
-
-
-
- Close
-
- }
- >
- {(triggerRef) => (
-
-
-
- )}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/features/room/MembersDrawer.tsx b/src/app/features/room/MembersDrawer.tsx
index d9205076..46d2238a 100644
--- a/src/app/features/room/MembersDrawer.tsx
+++ b/src/app/features/room/MembersDrawer.tsx
@@ -51,7 +51,7 @@ import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useMembershipFilter, useMembershipFilterMenu } from '../../hooks/useMemberFilter';
import { useMemberPowerSort, useMemberSort, useMemberSortMenu } from '../../hooks/useMemberSort';
-import { useGetMemberPowerLevel, usePowerLevelsContext } from '../../hooks/usePowerLevels';
+import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { MembershipFilterMenu } from '../../components/MembershipFilterMenu';
import { MemberSortMenu } from '../../components/MemberSortMenu';
import { useOpenUserRoomProfile, useUserRoomProfileState } from '../../state/hooks/userRoomProfile';
@@ -185,7 +185,6 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
const powerLevels = usePowerLevelsContext();
const creators = useRoomCreators(room);
const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
- const getPowerLevel = useGetMemberPowerLevel(powerLevels);
const fetchingMembers = members.length < room.getJoinedMemberCount();
const openUserRoomProfile = useOpenUserRoomProfile();
@@ -199,7 +198,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
const membershipFilter = useMembershipFilter(membershipFilterIndex, membershipFilterMenu);
const memberSort = useMemberSort(sortFilterIndex, sortFilterMenu);
- const memberPowerSort = useMemberPowerSort(creators, getPowerLevel);
+ const memberPowerSort = useMemberPowerSort(creators);
const typingMembers = useRoomTypingMember(room.roomId);
diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx
index ae42da81..47549fc7 100644
--- a/src/app/features/room/Room.tsx
+++ b/src/app/features/room/Room.tsx
@@ -2,7 +2,6 @@ import React, { useCallback } from 'react';
import { Box, Line } from 'folds';
import { useParams } from 'react-router-dom';
import { isKeyHotkey } from 'is-hotkey';
-import { useAtomValue } from 'jotai';
import { RoomView } from './RoomView';
import { MembersDrawer } from './MembersDrawer';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
@@ -14,28 +13,21 @@ import { useKeyDown } from '../../hooks/useKeyDown';
import { markAsRead } from '../../utils/notifications';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomMembers } from '../../hooks/useRoomMembers';
-import { CallView } from '../call/CallView';
-import { RoomViewHeader } from './RoomViewHeader';
-import { callChatAtom } from '../../state/callEmbed';
-import { CallChatView } from './CallChatView';
-import { useCallEmbed } from '../../hooks/useCallEmbed';
-import { useCallMembers, useCallSession } from '../../hooks/useCall';
+import { ThreadView } from './thread-view';
+import { useActiveThread } from '../../state/hooks/roomToActiveThread';
export function Room() {
const { eventId } = useParams();
const room = useRoom();
const mx = useMatrixClient();
- const callSession = useCallSession(room);
- const callMembers = useCallMembers(callSession);
- const callEmbed = useCallEmbed();
-
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const screenSize = useScreenSizeContext();
const powerLevels = usePowerLevels(room);
const members = useRoomMembers(mx, room.roomId);
- const chat = useAtomValue(callChatAtom);
+
+ const threadId = useActiveThread(room.roomId);
useKeyDown(
window,
@@ -49,41 +41,23 @@ export function Room() {
)
);
- const callView = callEmbed?.roomId === room.roomId || room.isCallRoom() || callMembers.length > 0;
-
return (
- {callView && (screenSize === ScreenSize.Desktop || !chat) && (
-
-
-
-
-
-
- )}
- {!callView && (
-
-
-
-
-
-
- )}
-
- {callView && chat && (
+
+ {threadId ? (
<>
- {screenSize === ScreenSize.Desktop && (
+
+
+ >
+ ) : (
+ screenSize === ScreenSize.Desktop &&
+ isDrawer && (
+ <>
- )}
-
- >
- )}
- {!callView && screenSize === ScreenSize.Desktop && isDrawer && (
- <>
-
-
- >
+
+ >
+ )
)}
diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx
index 46675428..ae46d2d0 100644
--- a/src/app/features/room/RoomInput.tsx
+++ b/src/app/features/room/RoomInput.tsx
@@ -39,7 +39,7 @@ import {
AutocompletePrefix,
AutocompleteQuery,
getAutocompleteQuery,
- getPrevWordRange,
+ getPrevWorldRange,
resetEditor,
RoomMentionAutocomplete,
UserMentionAutocomplete,
@@ -221,7 +221,7 @@ export const RoomInput = forwardRef(
const isComposing = useComposingCheck();
useElementSizeObserver(
- useCallback(() => fileDropContainerRef.current, [fileDropContainerRef]),
+ useCallback(() => document.body, []),
useCallback((width) => setHideStickerBtn(width < 500), [])
);
@@ -411,7 +411,7 @@ export const RoomInput = forwardRef(
sendTypingStatus(!isEmptyEditor(editor));
}
- const prevWordRange = getPrevWordRange(editor);
+ const prevWordRange = getPrevWorldRange(editor);
const query = prevWordRange
? getAutocompleteQuery(editor, prevWordRange, AUTOCOMPLETE_PREFIXES)
: undefined;
diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx
index c533528d..5ca2fc8e 100644
--- a/src/app/features/room/RoomTimeline.tsx
+++ b/src/app/features/room/RoomTimeline.tsx
@@ -77,6 +77,7 @@ import {
decryptAllTimelineEvent,
getEditedEvent,
getEventReactions,
+ getEventThreadDetail,
getLatestEditableEvt,
getMemberDisplayName,
getReactionContent,
@@ -126,6 +127,18 @@ import { useAccessiblePowerTagColors, useGetMemberPowerTag } from '../../hooks/u
import { useTheme } from '../../hooks/useTheme';
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
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>(
({ position, className, ...props }, ref) => (
@@ -150,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 = {
room: Room;
eventId?: string;
@@ -232,6 +172,14 @@ type RoomTimelineProps = {
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 = {
linkedTimelines: EventTimeline[];
range: ItemRange;
@@ -471,7 +419,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const permissions = useRoomPermissions(creators, powerLevels);
const canRedact = permissions.action('redact', mx.getSafeUserId());
- const canDeleteOwn = permissions.event(MessageEvent.RoomRedaction, mx.getSafeUserId());
const canSendReaction = permissions.event(MessageEvent.Reaction, mx.getSafeUserId());
const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, mx.getSafeUserId());
const [editId, setEditId] = useState();
@@ -483,6 +430,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const spoilerClickHandler = useSpoilerClickHandler();
const openUserRoomProfile = useOpenUserRoomProfile();
const space = useSpaceOptionally();
+ const handleThreadClick = useThreadSelector(room.roomId);
const imagePackRooms: Room[] = useImagePackRooms(room.roomId, roomToParents);
@@ -1015,6 +963,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
},
[editor]
);
+
const { t } = useTranslation();
const renderMatrixEvent = useMatrixEventRenderer<
@@ -1035,6 +984,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const senderId = mEvent.getSender() ?? '';
const senderDisplayName =
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
+ const threadDetail = getEventThreadDetail(mEvent);
return (
)}
+
+ {threadDetail && (
+
+
+
+ )}
);
},
@@ -1130,7 +1094,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
collapse={collapse}
highlight={highlighted}
edit={editId === mEventId}
- canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
+ canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
@@ -1248,7 +1212,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
messageLayout={messageLayout}
collapse={collapse}
highlight={highlighted}
- canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
+ canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
@@ -1469,57 +1433,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
);
},
- [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();
- const prevContent = mEvent.getPrevContent();
-
- const callJoined = content.application;
- if (callJoined && 'application' in prevContent) {
- return null;
- }
-
- const timeJSX = (
-
- );
-
- return (
-
-
-
- {senderName}
- {callJoined ? ' joined the call' : ' ended the call'}
-
-
- }
- />
-
- );
- },
},
(mEventId, mEvent, item) => {
if (!showHiddenEvents) return null;
diff --git a/src/app/features/room/RoomTombstone.tsx b/src/app/features/room/RoomTombstone.tsx
index 3556e6fc..24f0b80e 100644
--- a/src/app/features/room/RoomTombstone.tsx
+++ b/src/app/features/room/RoomTombstone.tsx
@@ -8,7 +8,6 @@ import { Membership } from '../../../types/matrix/room';
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { getViaServers } from '../../plugins/via-servers';
-import { joinEstateRoomOrThrow } from '../estate/joinEstateRoom';
type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string };
export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstoneProps) {
@@ -19,14 +18,16 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone
useCallback(() => {
const currentRoom = mx.getRoom(roomId);
const via = currentRoom ? getViaServers(currentRoom) : [];
- return joinEstateRoomOrThrow(mx, replacementRoomId, via);
+ return mx.joinRoom(replacementRoomId, {
+ viaServers: via,
+ });
}, [mx, roomId, replacementRoomId])
);
const replacementRoom = mx.getRoom(replacementRoomId);
const handleOpen = () => {
if (replacementRoom) navigateRoom(replacementRoom.roomId);
- if (joinState.status === AsyncStatus.Success) navigateRoom(joinState.data);
+ if (joinState.status === AsyncStatus.Success) navigateRoom(joinState.data.roomId);
};
return (
diff --git a/src/app/features/room/RoomView.tsx b/src/app/features/room/RoomView.tsx
index a4f0d7af..0f837594 100644
--- a/src/app/features/room/RoomView.tsx
+++ b/src/app/features/room/RoomView.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useRef } from 'react';
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 { isKeyHotkey } from 'is-hotkey';
import { useStateEvent } from '../../hooks/useStateEvent';
@@ -15,13 +15,13 @@ import { RoomTombstone } from './RoomTombstone';
import { RoomInput } from './RoomInput';
import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing';
import { Page } from '../../components/page';
+import { RoomViewHeader } from './RoomViewHeader';
import { useKeyDown } from '../../hooks/useKeyDown';
import { editableActiveElement } from '../../utils/dom';
import { settingsAtom } from '../../state/settings';
import { useSetting } from '../../state/hooks/settings';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { useRoomCreators } from '../../hooks/useRoomCreators';
-import { useRoom } from '../../hooks/useRoom';
const FN_KEYS_REGEX = /^F\d+$/;
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
@@ -30,8 +30,10 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
return false;
}
+ // do not focus on F keys
if (FN_KEYS_REGEX.test(code)) return false;
+ // do not focus on numlock/scroll lock
if (
code.startsWith('OS') ||
code.startsWith('Meta') ||
@@ -54,13 +56,12 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
return true;
};
-export function RoomView({ eventId }: { eventId?: string }) {
+export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
const roomInputRef = useRef(null);
const roomViewRef = useRef(null);
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
- const room = useRoom();
const { roomId } = room;
const editor = useEditor();
@@ -92,6 +93,7 @@ export function RoomView({ eventId }: { eventId?: string }) {
return (
+
(({ room, requestClose
);
});
-type CallMenuProps = {
- onVoiceCall: () => void;
- onVideoCall: () => void;
- requestClose: () => void;
-};
-const CallMenu = forwardRef(
- ({ requestClose, onVoiceCall, onVideoCall }, ref) => {
- const handleVoice = () => {
- onVoiceCall();
- requestClose();
- };
- const handleVideo = () => {
- onVideoCall();
- requestClose();
- };
-
- return (
-
-
- Start Call
-
- }
- onClick={handleVoice}
- >
- Voice
-
- }
- onClick={handleVideo}
- >
- Video
-
-
-
-
- );
- }
-);
-
-function CallButton() {
- const room = useRoom();
- const direct = useIsDirectRoom();
-
- const callEmbed = useCallEmbed();
- const startCall = useCallStart(direct);
- const callStarted = callEmbed && callEmbed.roomId === room.roomId;
- const inAnotherCall = callEmbed && !callStarted;
- const [menuAnchor, setMenuAnchor] = useState();
-
- const handleOpenMenu: MouseEventHandler = (evt) => {
- setMenuAnchor(evt.currentTarget.getBoundingClientRect());
- };
-
- return (
- <>
-
- {inAnotherCall ? (
- Already in another call β End the current call to join!
- ) : (
- Call
- )}
-
- }
- >
- {(triggerRef) => (
- {
- evt.preventDefault();
- startCall(room, {
- microphone: true,
- video: true,
- sound: true,
- });
- }}
- disabled={inAnotherCall || callStarted}
- aria-pressed={!!menuAnchor}
- >
-
-
- )}
-
- setMenuAnchor(undefined),
- clickOutsideDeactivates: true,
- isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
- isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
- escapeDeactivates: stopPropagation,
- }}
- >
- startCall(room, { microphone: true, video: true, sound: true })}
- onVoiceCall={() => startCall(room, { microphone: true, video: false, sound: true })}
- requestClose={() => setMenuAnchor(undefined)}
- />
-
- }
- />
- >
- );
-}
-
-export function RoomViewHeader({ callView }: { callView?: boolean }) {
+export function RoomViewHeader() {
const navigate = useNavigate();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const screenSize = useScreenSizeContext();
const room = useRoom();
const space = useSpaceOptionally();
- const powerLevels = usePowerLevelsContext();
- const creators = useRoomCreators(room);
- const permissions = useRoomPermissions(creators, powerLevels);
-
- const hasCallPermission = permissions.stateEvent(
- StateEvent.GroupCallMemberPrefix,
- mx.getSafeUserId()
- );
- const livekitSupported = useLivekitSupport();
- const rtcSupported = webRTCSupported();
-
const [menuAnchor, setMenuAnchor] = useState();
const [pinMenuAnchor, setPinMenuAnchor] = useState();
- const direct = useIsDirectRoom();
+ const [threadsMenuAnchor, setThreadsMenuAnchor] = useState();
+ const mDirects = useAtomValue(mDirectAtom);
const pinnedEvents = useRoomPinnedEvents(room);
const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption);
- const encryptedRoom = !!encryptionEvent;
- const avatarMxc = useRoomAvatar(room, direct);
+ const ecryptedRoom = !!encryptionEvent;
+ const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
const name = useRoomName(room);
const topic = useRoomTopic(room);
const avatarUrl = avatarMxc
@@ -435,27 +297,18 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
setPinMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
- const openSettings = useOpenRoomSettings();
- const parentSpace = useSpaceOptionally();
- const handleMemberToggle = () => {
- if (callView) {
- openSettings(room.roomId, parentSpace?.roomId, RoomSettingsPage.MembersPage);
- return;
- }
- setPeopleDrawer(!peopleDrawer);
+ const handleOpenThreadsMenu: MouseEventHandler = (evt) => {
+ setThreadsMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
return (
-
+
{screenSize === ScreenSize.Mobile && (
{(onBack) => (
-
+
@@ -470,7 +323,11 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
src={avatarUrl}
alt={name}
renderFallback={() => (
-
+
)}
/>
@@ -518,9 +375,8 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
)}
-
- {!encryptedRoom && (
+ {!ecryptedRoom && (
{(triggerRef) => (
-
+
)}
@@ -548,7 +404,6 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
>
{(triggerRef) => (
)}
+
+
+ My Threads
+
+ }
+ >
+ {(triggerRef) => (
+
+
+
+ )}
+
}
/>
- {!room.isCallRoom() && livekitSupported && rtcSupported && hasCallPermission && (
-
- )}
+ setThreadsMenuAnchor(undefined),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
+ isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+ setThreadsMenuAnchor(undefined)} />
+
+ }
+ />
{screenSize === ScreenSize.Desktop && (
- {callView ? (
- Members
- ) : (
- {peopleDrawer ? 'Hide Members' : 'Show Members'}
- )}
+ {peopleDrawer ? 'Hide Members' : 'Show Members'}
}
>
{(triggerRef) => (
-
+ setPeopleDrawer((drawer) => !drawer)}>
)}
)}
-
{(triggerRef) => (
-
+
)}
diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx
index 2d87e9bd..9324e1c2 100644
--- a/src/app/features/room/message/Message.tsx
+++ b/src/app/features/room/message/Message.tsx
@@ -327,9 +327,11 @@ export const MessageCopyLinkItem = as<
const mx = useMatrixClient();
const handleCopy = () => {
+ const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
const eventId = mEvent.getId();
+ const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
if (!eventId) return;
- copyToClipboard(getMatrixToRoomEvent(room.roomId, eventId, getViaServers(room)));
+ copyToClipboard(getMatrixToRoomEvent(roomIdOrAlias, eventId, viaServers));
onClose?.();
};
diff --git a/src/app/features/room/message/MessageEditor.tsx b/src/app/features/room/message/MessageEditor.tsx
index 2d7f2097..9a7567aa 100644
--- a/src/app/features/room/message/MessageEditor.tsx
+++ b/src/app/features/room/message/MessageEditor.tsx
@@ -35,7 +35,7 @@ import {
createEmoticonElement,
customHtmlEqualsPlainText,
getAutocompleteQuery,
- getPrevWordRange,
+ getPrevWorldRange,
htmlToEditorInput,
moveCursor,
plainToEditorInput,
@@ -187,7 +187,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
return;
}
- const prevWordRange = getPrevWordRange(editor);
+ const prevWordRange = getPrevWorldRange(editor);
const query = prevWordRange
? getAutocompleteQuery(editor, prevWordRange, AUTOCOMPLETE_PREFIXES)
: undefined;
diff --git a/src/app/features/room/message/thread-selector/ThreadSelector.tsx b/src/app/features/room/message/thread-selector/ThreadSelector.tsx
new file mode 100644
index 00000000..41c87edc
--- /dev/null
+++ b/src/app/features/room/message/thread-selector/ThreadSelector.tsx
@@ -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 {children} ;
+}
+
+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 (
+ onClick?.(threadId)}
+ >
+
+
+
+ {threadDetail.count} {threadDetail.count === 1 ? 'Reply' : 'Replies'}
+
+
+ {latestSenderId && (
+ <>
+
+
+
+ Last reply by
+ {latestDisplayName}
+ β
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/src/app/features/room/message/thread-selector/index.ts b/src/app/features/room/message/thread-selector/index.ts
new file mode 100644
index 00000000..2dfa5d9e
--- /dev/null
+++ b/src/app/features/room/message/thread-selector/index.ts
@@ -0,0 +1 @@
+export * from './ThreadSelector';
diff --git a/src/app/features/room/message/thread-selector/styles.css.ts b/src/app/features/room/message/thread-selector/styles.css.ts
new file mode 100644
index 00000000..45bb7d62
--- /dev/null
+++ b/src/app/features/room/message/thread-selector/styles.css.ts
@@ -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,
+});
diff --git a/src/app/features/room/thread-view/ThreadView.tsx b/src/app/features/room/thread-view/ThreadView.tsx
new file mode 100644
index 00000000..6b6f4642
--- /dev/null
+++ b/src/app/features/room/thread-view/ThreadView.tsx
@@ -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 (
+
+
+
+
+
+
+ Thread
+
+
+
+
+ Close
+
+ }
+ >
+ {(triggerRef) => (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {events.map((mEvent) => (
+
+ {mEvent.sender?.name}: {mEvent.getContent().body}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/app/features/room/thread-view/index.tsx b/src/app/features/room/thread-view/index.tsx
new file mode 100644
index 00000000..6ba05aab
--- /dev/null
+++ b/src/app/features/room/thread-view/index.tsx
@@ -0,0 +1 @@
+export * from './ThreadView';
diff --git a/src/app/features/room/thread-view/styles.css.ts b/src/app/features/room/thread-view/styles.css.ts
new file mode 100644
index 00000000..fe66d49b
--- /dev/null
+++ b/src/app/features/room/thread-view/styles.css.ts
@@ -0,0 +1,21 @@
+import { style } from '@vanilla-extract/css';
+import { config, toRem } from 'folds';
+
+export const ThreadView = style({
+ width: toRem(456),
+ flexShrink: 0,
+ flexGrow: 0,
+});
+
+export const ThreadViewFloating = style({
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ bottom: 0,
+ zIndex: 1,
+
+ maxWidth: toRem(456),
+ flexShrink: 1,
+ width: '100vw',
+ boxShadow: config.shadow.E400,
+});
diff --git a/src/app/features/room/threads-menu/ThreadsError.tsx b/src/app/features/room/threads-menu/ThreadsError.tsx
new file mode 100644
index 00000000..c72b7202
--- /dev/null
+++ b/src/app/features/room/threads-menu/ThreadsError.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Box, Icon, Icons, toRem, Text, config } from 'folds';
+import { ContainerColor } from '../../../styles/ContainerColor.css';
+import { BreakWord } from '../../../styles/Text.css';
+
+export function ThreadsError({ error }: { error: Error }) {
+ return (
+
+
+
+
+ {error.name}
+
+
+ {error.message}
+
+
+
+ );
+}
diff --git a/src/app/features/room/threads-menu/ThreadsLoading.tsx b/src/app/features/room/threads-menu/ThreadsLoading.tsx
new file mode 100644
index 00000000..236435b2
--- /dev/null
+++ b/src/app/features/room/threads-menu/ThreadsLoading.tsx
@@ -0,0 +1,23 @@
+import { Box, config, Spinner } from 'folds';
+import React from 'react';
+import { ContainerColor } from '../../../styles/ContainerColor.css';
+
+export function ThreadsLoading() {
+ return (
+
+
+
+ );
+}
diff --git a/src/app/features/room/threads-menu/ThreadsMenu.css.ts b/src/app/features/room/threads-menu/ThreadsMenu.css.ts
new file mode 100644
index 00000000..96cb1725
--- /dev/null
+++ b/src/app/features/room/threads-menu/ThreadsMenu.css.ts
@@ -0,0 +1,18 @@
+import { style } from '@vanilla-extract/css';
+import { config, toRem } from 'folds';
+
+export const ThreadsMenu = style({
+ display: 'flex',
+ maxWidth: toRem(548),
+ width: '100vw',
+ maxHeight: '90vh',
+});
+
+export const ThreadsMenuHeader = style({
+ paddingLeft: config.space.S400,
+ paddingRight: config.space.S200,
+});
+
+export const ThreadsMenuContent = style({
+ paddingLeft: config.space.S200,
+});
diff --git a/src/app/features/room/threads-menu/ThreadsMenu.tsx b/src/app/features/room/threads-menu/ThreadsMenu.tsx
new file mode 100644
index 00000000..28e73f33
--- /dev/null
+++ b/src/app/features/room/threads-menu/ThreadsMenu.tsx
@@ -0,0 +1,99 @@
+/* eslint-disable react/destructuring-assignment */
+import React, { forwardRef, useMemo } from 'react';
+import { EventTimelineSet, Room } from 'matrix-js-sdk';
+import { Box, config, Header, Icon, IconButton, Icons, Menu, Scroll, Text, toRem } from 'folds';
+import * as css from './ThreadsMenu.css';
+import { ContainerColor } from '../../../styles/ContainerColor.css';
+import { useRoomMyThreads } from '../../../hooks/useRoomThreads';
+import { AsyncStatus } from '../../../hooks/useAsyncCallback';
+import { getLinkedTimelines, getTimelinesEventsCount } from '../utils';
+import { ThreadsTimeline } from './ThreadsTimeline';
+import { ThreadsLoading } from './ThreadsLoading';
+import { ThreadsError } from './ThreadsError';
+
+const getTimelines = (timelineSet: EventTimelineSet) => {
+ const liveTimeline = timelineSet.getLiveTimeline();
+ const linkedTimelines = getLinkedTimelines(liveTimeline);
+
+ return linkedTimelines;
+};
+
+function NoThreads() {
+ return (
+
+
+
+
+ No Threads Yet
+
+
+ Threads youβre participating in will appear here.
+
+
+
+ );
+}
+
+type ThreadsMenuProps = {
+ room: Room;
+ requestClose: () => void;
+};
+export const ThreadsMenu = forwardRef(
+ ({ room, requestClose }, ref) => {
+ const threadsState = useRoomMyThreads(room);
+ const threadsTimelineSet =
+ threadsState.status === AsyncStatus.Success ? threadsState.data : undefined;
+
+ const linkedTimelines = useMemo(() => {
+ if (!threadsTimelineSet) return undefined;
+ return getTimelines(threadsTimelineSet);
+ }, [threadsTimelineSet]);
+
+ const hasEvents = linkedTimelines && getTimelinesEventsCount(linkedTimelines) > 0;
+
+ return (
+
+
+
+
+ My Threads
+
+
+
+
+
+
+
+
+ {threadsState.status === AsyncStatus.Success && hasEvents ? (
+
+ ) : (
+
+
+ {(threadsState.status === AsyncStatus.Loading ||
+ threadsState.status === AsyncStatus.Idle) && }
+ {threadsState.status === AsyncStatus.Success && !hasEvents && }
+ {threadsState.status === AsyncStatus.Error && (
+
+ )}
+
+
+ )}
+
+
+
+ );
+ }
+);
diff --git a/src/app/features/room/threads-menu/ThreadsTimeline.tsx b/src/app/features/room/threads-menu/ThreadsTimeline.tsx
new file mode 100644
index 00000000..0e28f97d
--- /dev/null
+++ b/src/app/features/room/threads-menu/ThreadsTimeline.tsx
@@ -0,0 +1,488 @@
+/* eslint-disable react/destructuring-assignment */
+import React, { MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { Direction, EventTimeline, MatrixEvent, Room } from 'matrix-js-sdk';
+import { Avatar, Box, Chip, config, Icon, Icons, Scroll, Text } from 'folds';
+import { Opts as LinkifyOpts } from 'linkifyjs';
+import { HTMLReactParserOptions } from 'html-react-parser';
+import { useVirtualizer } from '@tanstack/react-virtual';
+import { SequenceCard } from '../../../components/sequence-card';
+import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import {
+ AvatarBase,
+ ImageContent,
+ MessageNotDecryptedContent,
+ MessageUnsupportedContent,
+ ModernLayout,
+ MSticker,
+ RedactedContent,
+ Reply,
+ Time,
+ Username,
+ UsernameBold,
+} from '../../../components/message';
+import { UserAvatar } from '../../../components/user-avatar';
+import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import {
+ getEditedEvent,
+ getEventThreadDetail,
+ getMemberAvatarMxc,
+ getMemberDisplayName,
+} from '../../../utils/room';
+import { GetContentCallback, MessageEvent } from '../../../../types/matrix/room';
+import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
+import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
+import {
+ factoryRenderLinkifyWithMention,
+ getReactCustomHtmlParser,
+ LINKIFY_OPTS,
+ makeMentionCustomProps,
+ renderMatrixMention,
+} from '../../../plugins/react-custom-html-parser';
+import { RenderMatrixEvent, useMatrixEventRenderer } from '../../../hooks/useMatrixEventRenderer';
+import { RenderMessageContent } from '../../../components/RenderMessageContent';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
+import * as customHtmlCss from '../../../styles/CustomHtml.css';
+import { EncryptedContent } from '../message';
+import { Image } from '../../../components/media';
+import { ImageViewer } from '../../../components/image-viewer';
+import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
+import { usePowerLevelsContext } from '../../../hooks/usePowerLevels';
+import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
+import { useTheme } from '../../../hooks/useTheme';
+import { PowerIcon } from '../../../components/power';
+import colorMXID from '../../../../util/colorMXID';
+import { useIsDirectRoom, useRoom } from '../../../hooks/useRoom';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import {
+ GetMemberPowerTag,
+ getPowerTagIconSrc,
+ useAccessiblePowerTagColors,
+ useGetMemberPowerTag,
+} from '../../../hooks/useMemberPowerTag';
+import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
+import { ThreadSelector, ThreadSelectorContainer } from '../message/thread-selector';
+import {
+ getTimelineAndBaseIndex,
+ getTimelineEvent,
+ getTimelineRelativeIndex,
+ getTimelinesEventsCount,
+} from '../utils';
+import { ThreadsLoading } from './ThreadsLoading';
+import { VirtualTile } from '../../../components/virtualizer';
+import { useAlive } from '../../../hooks/useAlive';
+import * as css from './ThreadsMenu.css';
+
+type ThreadMessageProps = {
+ room: Room;
+ event: MatrixEvent;
+ renderContent: RenderMatrixEvent<[string, MatrixEvent, string, GetContentCallback]>;
+ onOpen: (roomId: string, eventId: string) => void;
+ getMemberPowerTag: GetMemberPowerTag;
+ accessibleTagColors: Map;
+ legacyUsernameColor: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
+};
+function ThreadMessage({
+ room,
+ event,
+ renderContent,
+ onOpen,
+ getMemberPowerTag,
+ accessibleTagColors,
+ legacyUsernameColor,
+ hour24Clock,
+ dateFormatString,
+}: ThreadMessageProps) {
+ const useAuthentication = useMediaAuthentication();
+ const mx = useMatrixClient();
+
+ const handleOpenClick: MouseEventHandler = (evt) => {
+ evt.stopPropagation();
+ const evtId = evt.currentTarget.getAttribute('data-event-id');
+ if (!evtId) return;
+ onOpen(room.roomId, evtId);
+ };
+
+ const renderOptions = () => (
+
+
+ Open
+
+
+ );
+
+ const sender = event.getSender()!;
+ const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender;
+ const senderAvatarMxc = getMemberAvatarMxc(room, sender);
+ const getContent = (() => event.getContent()) as GetContentCallback;
+
+ const memberPowerTag = getMemberPowerTag(sender);
+ const tagColor = memberPowerTag?.color
+ ? accessibleTagColors?.get(memberPowerTag.color)
+ : undefined;
+ const tagIconSrc = memberPowerTag?.icon
+ ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
+ : undefined;
+
+ const usernameColor = legacyUsernameColor ? colorMXID(sender) : tagColor;
+
+ const mEventId = event.getId();
+
+ if (!mEventId) return null;
+
+ return (
+
+
+ }
+ />
+
+
+ }
+ >
+
+
+
+
+
+ {displayName}
+
+
+ {tagIconSrc && }
+
+
+
+ {renderOptions()}
+
+ {event.replyEventId && (
+
+ )}
+ {renderContent(event.getType(), false, mEventId, event, displayName, getContent)}
+
+ );
+}
+
+type ThreadsTimelineProps = {
+ timelines: EventTimeline[];
+ requestClose: () => void;
+};
+export function ThreadsTimeline({ timelines, requestClose }: ThreadsTimelineProps) {
+ const mx = useMatrixClient();
+ const { navigateRoom } = useRoomNavigate();
+ const alive = useAlive();
+ const scrollRef = useRef(null);
+
+ const room = useRoom();
+ const powerLevels = usePowerLevelsContext();
+ const creators = useRoomCreators(room);
+
+ const creatorsTag = useRoomCreatorsTag();
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
+ const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+
+ const theme = useTheme();
+ const accessibleTagColors = useAccessiblePowerTagColors(theme.kind, creatorsTag, powerLevelTags);
+
+ const useAuthentication = useMediaAuthentication();
+ const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
+ const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
+
+ const direct = useIsDirectRoom();
+ const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
+
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
+ const mentionClickHandler = useMentionClickHandler(room.roomId);
+ const spoilerClickHandler = useSpoilerClickHandler();
+
+ const linkifyOpts = useMemo(
+ () => ({
+ ...LINKIFY_OPTS,
+ render: factoryRenderLinkifyWithMention((href) =>
+ renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
+ ),
+ }),
+ [mx, room, mentionClickHandler]
+ );
+ const htmlReactParserOptions = useMemo(
+ () =>
+ getReactCustomHtmlParser(mx, room.roomId, {
+ linkifyOpts,
+ useAuthentication,
+ handleSpoilerClick: spoilerClickHandler,
+ handleMentionClick: mentionClickHandler,
+ }),
+ [mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication]
+ );
+
+ const renderMatrixEvent = useMatrixEventRenderer<
+ [string, MatrixEvent, string, GetContentCallback]
+ >(
+ {
+ [MessageEvent.RoomMessage]: (eventId, event, displayName, getContent) => {
+ if (event.isRedacted()) {
+ return ;
+ }
+
+ const threadDetail = getEventThreadDetail(event);
+
+ return (
+ <>
+
+ {threadDetail && (
+
+
+
+ )}
+ >
+ );
+ },
+ [MessageEvent.RoomMessageEncrypted]: (eventId, mEvent, displayName) => {
+ const evtTimeline = room.getTimelineForEvent(eventId);
+
+ return (
+
+ {() => {
+ if (mEvent.isRedacted()) return ;
+ if (mEvent.getType() === MessageEvent.Sticker)
+ return (
+ (
+ }
+ renderViewer={(p) => }
+ />
+ )}
+ />
+ );
+ if (mEvent.getType() === MessageEvent.RoomMessage) {
+ const editedEvent =
+ evtTimeline && getEditedEvent(eventId, mEvent, evtTimeline.getTimelineSet());
+ const getContent = (() =>
+ editedEvent?.getContent()['m.new_content'] ??
+ mEvent.getContent()) as GetContentCallback;
+
+ return (
+
+ );
+ }
+ if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
+ return (
+
+
+
+ );
+ return (
+
+
+
+ );
+ }}
+
+ );
+ },
+ [MessageEvent.Sticker]: (eventId, event, displayName, getContent) => {
+ if (event.isRedacted()) {
+ return ;
+ }
+
+ const threadDetail = getEventThreadDetail(event);
+
+ return (
+ <>
+ (
+ }
+ renderViewer={(p) => }
+ />
+ )}
+ />
+ {threadDetail && (
+
+
+
+ )}
+ >
+ );
+ },
+ },
+ undefined,
+ (eventId, event) => {
+ if (event.isRedacted()) {
+ return ;
+ }
+ return (
+
+
+ {event.getType()}
+ {' event'}
+
+
+ );
+ }
+ );
+
+ const handleOpen = (roomId: string, eventId: string) => {
+ navigateRoom(roomId, eventId);
+ requestClose();
+ };
+
+ const eventsLength = getTimelinesEventsCount(timelines);
+ const timelineToPaginate = timelines[timelines.length - 1];
+ const [paginationToken, setPaginationToken] = useState(
+ timelineToPaginate.getPaginationToken(Direction.Backward)
+ );
+
+ const virtualizer = useVirtualizer({
+ count: eventsLength,
+ getScrollElement: () => scrollRef.current,
+ estimateSize: () => 122,
+ overscan: 10,
+ });
+ const vItems = virtualizer.getVirtualItems();
+
+ const paginate = useCallback(async () => {
+ const moreToLoad = await mx.paginateEventTimeline(timelineToPaginate, {
+ backwards: true,
+ limit: 30,
+ });
+
+ if (alive()) {
+ setPaginationToken(
+ moreToLoad ? timelineToPaginate.getPaginationToken(Direction.Backward) : null
+ );
+ }
+ }, [mx, alive, timelineToPaginate]);
+
+ // auto paginate when scroll reach bottom
+ useEffect(() => {
+ const lastVItem = vItems.length > 0 ? vItems[vItems.length - 1] : undefined;
+ if (paginationToken && lastVItem && lastVItem.index === eventsLength - 1) {
+ paginate();
+ }
+ }, [vItems, paginationToken, eventsLength, paginate]);
+
+ return (
+
+
+
+ {vItems.map((vItem) => {
+ const reverseTimelineIndex = eventsLength - vItem.index - 1;
+
+ const [timeline, baseIndex] = getTimelineAndBaseIndex(timelines, reverseTimelineIndex);
+ if (!timeline) return null;
+ const event = getTimelineEvent(
+ timeline,
+ getTimelineRelativeIndex(reverseTimelineIndex, baseIndex)
+ );
+
+ if (!event?.getId()) return null;
+
+ return (
+
+
+
+
+
+ );
+ })}
+
+ {paginationToken && }
+
+
+ );
+}
diff --git a/src/app/features/room/threads-menu/index.ts b/src/app/features/room/threads-menu/index.ts
new file mode 100644
index 00000000..98286736
--- /dev/null
+++ b/src/app/features/room/threads-menu/index.ts
@@ -0,0 +1 @@
+export * from './ThreadsMenu';
diff --git a/src/app/features/room/utils/index.ts b/src/app/features/room/utils/index.ts
new file mode 100644
index 00000000..13d812d7
--- /dev/null
+++ b/src/app/features/room/utils/index.ts
@@ -0,0 +1 @@
+export * from './timeline';
diff --git a/src/app/features/room/utils/timeline.ts b/src/app/features/room/utils/timeline.ts
new file mode 100644
index 00000000..f9c45525
--- /dev/null
+++ b/src/app/features/room/utils/timeline.ts
@@ -0,0 +1,66 @@
+import { Room, EventTimeline, Direction, MatrixEvent } from 'matrix-js-sdk';
+
+export const getFirstLinkedTimeline = (
+ timeline: EventTimeline,
+ direction: Direction
+): EventTimeline => {
+ const linkedTm = timeline.getNeighbouringTimeline(direction);
+ if (!linkedTm) return timeline;
+ return getFirstLinkedTimeline(linkedTm, direction);
+};
+
+export const getLinkedTimelines = (timeline: EventTimeline): EventTimeline[] => {
+ const firstTimeline = getFirstLinkedTimeline(timeline, Direction.Backward);
+ const timelines: EventTimeline[] = [];
+
+ for (
+ let nextTimeline: EventTimeline | null = firstTimeline;
+ nextTimeline;
+ nextTimeline = nextTimeline.getNeighbouringTimeline(Direction.Forward)
+ ) {
+ timelines.push(nextTimeline);
+ }
+ return timelines;
+};
+
+export const timelineToEventsCount = (t: EventTimeline) => t.getEvents().length;
+export const getTimelinesEventsCount = (timelines: EventTimeline[]): number => {
+ const timelineEventCountReducer = (count: number, tm: EventTimeline) =>
+ count + timelineToEventsCount(tm);
+ return timelines.reduce(timelineEventCountReducer, 0);
+};
+
+export const getTimelineAndBaseIndex = (
+ timelines: EventTimeline[],
+ index: number
+): [EventTimeline | undefined, number] => {
+ let uptoTimelineLen = 0;
+ const timeline = timelines.find((t) => {
+ uptoTimelineLen += t.getEvents().length;
+ if (index < uptoTimelineLen) return true;
+ return false;
+ });
+ if (!timeline) return [undefined, 0];
+ return [timeline, uptoTimelineLen - timeline.getEvents().length];
+};
+
+export const getTimelineRelativeIndex = (absoluteIndex: number, timelineBaseIndex: number) =>
+ absoluteIndex - timelineBaseIndex;
+
+export const getTimelineEvent = (timeline: EventTimeline, index: number): MatrixEvent | undefined =>
+ timeline.getEvents()[index];
+
+export const getEventIdAbsoluteIndex = (
+ timelines: EventTimeline[],
+ eventTimeline: EventTimeline,
+ eventId: string
+): number | undefined => {
+ const timelineIndex = timelines.findIndex((t) => t === eventTimeline);
+ if (timelineIndex === -1) return undefined;
+ const eventIndex = eventTimeline.getEvents().findIndex((evt) => evt.getId() === eventId);
+ if (eventIndex === -1) return undefined;
+ const baseIndex = timelines
+ .slice(0, timelineIndex)
+ .reduce((accValue, timeline) => timeline.getEvents().length + accValue, 0);
+ return baseIndex + eventIndex;
+};
diff --git a/src/app/features/search/Search.tsx b/src/app/features/search/Search.tsx
index cfc4ee5f..fcd6233a 100644
--- a/src/app/features/search/Search.tsx
+++ b/src/app/features/search/Search.tsx
@@ -286,7 +286,7 @@ export function Search({ requestClose }: SearchProps) {
gap="100"
>
- {result ? 'No Match Found' : 'No Rooms'}
+ {result ? 'No Match Found' : `No Rooms'}`}
{result
@@ -373,7 +373,7 @@ export function Search({ requestClose }: SearchProps) {
)}
diff --git a/src/app/features/settings/about/About.tsx b/src/app/features/settings/about/About.tsx
index d3dcde96..4fba564e 100644
--- a/src/app/features/settings/about/About.tsx
+++ b/src/app/features/settings/about/About.tsx
@@ -46,7 +46,7 @@ export function About({ requestClose }: AboutProps) {
Cinny
- v4.12.8
+ v4.10.0
Yet another matrix client.
diff --git a/src/app/features/space-settings/SpaceSettings.tsx b/src/app/features/space-settings/SpaceSettings.tsx
index b5fefc93..e565fb92 100644
--- a/src/app/features/space-settings/SpaceSettings.tsx
+++ b/src/app/features/space-settings/SpaceSettings.tsx
@@ -103,7 +103,7 @@ export function SpaceSettings({ initialPage, requestClose }: SpaceSettingsProps)
alt={roomName}
renderFallback={() => (
- Advanced Options
+ Advance Options
diff --git a/src/app/features/space-settings/permissions/usePermissionItems.ts b/src/app/features/space-settings/permissions/usePermissionItems.ts
index a02cf420..8192e7bc 100644
--- a/src/app/features/space-settings/permissions/usePermissionItems.ts
+++ b/src/app/features/space-settings/permissions/usePermissionItems.ts
@@ -125,13 +125,6 @@ export const usePermissionGroups = (): PermissionGroup[] => {
const otherSettingsGroup: PermissionGroup = {
name: 'Other',
items: [
- {
- location: {
- state: true,
- key: StateEvent.PoniesRoomEmotes,
- },
- name: 'Manage Emojis & Stickers',
- },
{
location: {
state: true,
diff --git a/src/app/hooks/router/useSelectedSpace.ts b/src/app/hooks/router/useSelectedSpace.ts
index b891fde7..2e8e73a0 100644
--- a/src/app/hooks/router/useSelectedSpace.ts
+++ b/src/app/hooks/router/useSelectedSpace.ts
@@ -18,7 +18,7 @@ export const useSelectedSpace = (): string | undefined => {
export const useSpaceLobbySelected = (spaceIdOrAlias: string): boolean => {
const match = useMatch({
- path: decodeURIComponent(getSpaceLobbyPath(spaceIdOrAlias)),
+ path: getSpaceLobbyPath(spaceIdOrAlias),
caseSensitive: true,
end: false,
});
@@ -28,7 +28,7 @@ export const useSpaceLobbySelected = (spaceIdOrAlias: string): boolean => {
export const useSpaceSearchSelected = (spaceIdOrAlias: string): boolean => {
const match = useMatch({
- path: decodeURIComponent(getSpaceSearchPath(spaceIdOrAlias)),
+ path: getSpaceSearchPath(spaceIdOrAlias),
caseSensitive: true,
end: false,
});
diff --git a/src/app/hooks/useAccountManagement.ts b/src/app/hooks/useAccountManagement.ts
index aeccd581..5eafedc4 100644
--- a/src/app/hooks/useAccountManagement.ts
+++ b/src/app/hooks/useAccountManagement.ts
@@ -4,9 +4,9 @@ export const useAccountManagementActions = () => {
const actions = useMemo(
() => ({
profile: 'org.matrix.profile',
- sessionsList: 'org.matrix.devices_list',
- sessionView: 'org.matrix.device_view',
- sessionEnd: 'org.matrix.device_delete',
+ sessionsList: 'org.matrix.sessions_list',
+ sessionView: 'org.matrix.session_view',
+ sessionEnd: 'org.matrix.session_end',
accountDeactivate: 'org.matrix.account_deactivate',
crossSigningReset: 'org.matrix.cross_signing_reset',
}),
diff --git a/src/app/hooks/useCall.ts b/src/app/hooks/useCall.ts
deleted file mode 100644
index 817e12fc..00000000
--- a/src/app/hooks/useCall.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { Room } from 'matrix-js-sdk';
-import {
- MatrixRTCSession,
- MatrixRTCSessionEvent,
- MatrixRTCSessionEventHandlerMap,
-} from 'matrix-js-sdk/lib/matrixrtc/MatrixRTCSession';
-import { CallMembership } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
-import { useEffect, useState } from 'react';
-import { MatrixRTCSessionManagerEvents } from 'matrix-js-sdk/lib/matrixrtc/MatrixRTCSessionManager';
-import { useMatrixClient } from './useMatrixClient';
-
-export const useCallSession = (room: Room): MatrixRTCSession => {
- const mx = useMatrixClient();
-
- const [session, setSession] = useState(mx.matrixRTC.getRoomSession(room));
-
- useEffect(() => {
- const start = (roomId: string) => {
- if (roomId !== room.roomId) return;
- setSession(mx.matrixRTC.getRoomSession(room));
- };
- const end = (roomId: string) => {
- if (roomId !== room.roomId) return;
- setSession(mx.matrixRTC.getRoomSession(room));
- };
- mx.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, start);
- mx.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, end);
- return () => {
- mx.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, start);
- mx.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, end);
- };
- }, [mx, room]);
-
- return session;
-};
-
-export const useCallMembersChange = (
- session: MatrixRTCSession,
- callback: (members: CallMembership[]) => void
-): void => {
- useEffect(() => {
- const handleMembershipsChange: MatrixRTCSessionEventHandlerMap[MatrixRTCSessionEvent.MembershipsChanged] =
- (oldestMembership, newMemberships) => {
- callback(newMemberships);
- };
-
- session.on(MatrixRTCSessionEvent.MembershipsChanged, handleMembershipsChange);
- return () => {
- session.removeListener(MatrixRTCSessionEvent.MembershipsChanged, handleMembershipsChange);
- };
- }, [session, callback]);
-};
-
-export const useCallMembers = (session: MatrixRTCSession): CallMembership[] => {
- const [memberships, setMemberships] = useState(session.memberships);
-
- useCallMembersChange(session, setMemberships);
-
- return memberships;
-};
diff --git a/src/app/hooks/useCallEmbed.ts b/src/app/hooks/useCallEmbed.ts
deleted file mode 100644
index af6f47b4..00000000
--- a/src/app/hooks/useCallEmbed.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import { createContext, RefObject, useCallback, useContext, useEffect, useState } from 'react';
-import { MatrixClient, Room } from 'matrix-js-sdk';
-import { useSetAtom } from 'jotai';
-import {
- CallEmbed,
- ElementCallThemeKind,
- ElementWidgetActions,
- useClientWidgetApiEvent,
-} from '../plugins/call';
-import { useMatrixClient } from './useMatrixClient';
-import { ThemeKind, useTheme } from './useTheme';
-import { callEmbedAtom } from '../state/callEmbed';
-import { useResizeObserver } from './useResizeObserver';
-import { CallControlState } from '../plugins/call/CallControlState';
-import { useCallMembersChange, useCallSession } from './useCall';
-import { CallPreferences } from '../state/callPreferences';
-
-const CallEmbedContext = createContext(undefined);
-
-export const CallEmbedContextProvider = CallEmbedContext.Provider;
-
-export const useCallEmbed = (): CallEmbed | undefined => {
- const callEmbed = useContext(CallEmbedContext);
-
- return callEmbed;
-};
-
-const CallEmbedRefContext = createContext | undefined>(undefined);
-export const CallEmbedRefContextProvider = CallEmbedRefContext.Provider;
-export const useCallEmbedRef = (): RefObject => {
- const ref = useContext(CallEmbedRefContext);
- if (!ref) {
- throw new Error('CallEmbedRef is not provided!');
- }
- return ref;
-};
-
-export const createCallEmbed = (
- mx: MatrixClient,
- room: Room,
- dm: boolean,
- themeKind: ElementCallThemeKind,
- container: HTMLElement,
- pref?: CallPreferences
-): CallEmbed => {
- const rtcSession = mx.matrixRTC.getRoomSession(room);
- const ongoing = rtcSession.memberships.length > 0;
-
- const intent = CallEmbed.getIntent(dm, ongoing, pref?.video);
- const widget = CallEmbed.getWidget(mx, room, intent, themeKind);
- const controlState = pref && new CallControlState(pref.microphone, pref.video, pref.sound);
-
- const embed = new CallEmbed(mx, room, widget, container, controlState);
-
- return embed;
-};
-
-export const useCallStart = (dm = false) => {
- const mx = useMatrixClient();
- const theme = useTheme();
- const setCallEmbed = useSetAtom(callEmbedAtom);
- const callEmbedRef = useCallEmbedRef();
-
- const startCall = useCallback(
- (room: Room, pref?: CallPreferences) => {
- const container = callEmbedRef.current;
- if (!container) {
- throw new Error('Failed to start call, No embed container element found!');
- }
- const callEmbed = createCallEmbed(mx, room, dm, theme.kind, container, pref);
-
- setCallEmbed(callEmbed);
- },
- [mx, dm, theme, setCallEmbed, callEmbedRef]
- );
-
- return startCall;
-};
-
-export const useCallJoined = (embed?: CallEmbed): boolean => {
- const [joined, setJoined] = useState(embed?.joined ?? false);
-
- useClientWidgetApiEvent(
- embed?.call,
- ElementWidgetActions.JoinCall,
- useCallback(() => {
- setJoined(true);
- }, [])
- );
-
- useEffect(() => {
- if (!embed) {
- setJoined(false);
- }
- }, [embed]);
-
- return joined;
-};
-
-export const useCallHangupEvent = (embed: CallEmbed, callback: () => void) => {
- useClientWidgetApiEvent(embed.call, ElementWidgetActions.HangupCall, callback);
- useClientWidgetApiEvent(embed.call, ElementWidgetActions.Close, callback);
-};
-
-export const useCallMemberSoundSync = (embed: CallEmbed) => {
- const callSession = useCallSession(embed.room);
- useCallMembersChange(
- callSession,
- useCallback(() => embed.control.applySound(), [embed])
- );
-};
-
-export const useCallThemeSync = (embed: CallEmbed) => {
- const theme = useTheme();
-
- useEffect(() => {
- const name: ElementCallThemeKind = theme.kind === ThemeKind.Dark ? 'dark' : 'light';
-
- embed.setTheme(name);
- }, [theme.kind, embed]);
-};
-
-export const useCallEmbedPlacementSync = (containerViewRef: RefObject): void => {
- const callEmbedRef = useCallEmbedRef();
-
- const syncCallEmbedPlacement = useCallback(() => {
- const embedEl = callEmbedRef.current;
- const container = containerViewRef.current;
- if (!embedEl || !container) return;
-
- embedEl.style.top = `${container.offsetTop}px`;
- embedEl.style.left = `${container.offsetLeft}px`;
- embedEl.style.width = `${container.clientWidth}px`;
- embedEl.style.height = `${container.clientHeight}px`;
- }, [callEmbedRef, containerViewRef]);
-
- useResizeObserver(
- syncCallEmbedPlacement,
- useCallback(() => containerViewRef.current, [containerViewRef])
- );
-};
diff --git a/src/app/hooks/useCallSpeakers.ts b/src/app/hooks/useCallSpeakers.ts
deleted file mode 100644
index 7f720074..00000000
--- a/src/app/hooks/useCallSpeakers.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useCallback, useEffect, useMemo, useState } from 'react';
-import { CallEmbed } from '../plugins/call';
-import { useMutationObserver } from './useMutationObserver';
-import { isUserId } from '../utils/matrix';
-import { useCallMembers, useCallSession } from './useCall';
-import { useCallJoined } from './useCallEmbed';
-
-export const useCallSpeakers = (callEmbed: CallEmbed): Set => {
- const [speakers, setSpeakers] = useState(new Set());
- const callSession = useCallSession(callEmbed.room);
- const callMembers = useCallMembers(callSession);
- const joined = useCallJoined(callEmbed);
-
- const videoContainers = useMemo(() => {
- if (callMembers && joined) return callEmbed.document?.querySelectorAll('[data-video-fit]');
- return undefined;
- }, [callEmbed, callMembers, joined]);
-
- const mutationObserver = useMutationObserver(
- useCallback(
- (mutations) => {
- const s = new Set();
-
- mutations.forEach((mutation) => {
- if (mutation.type !== 'attributes') return;
- const el = mutation.target as HTMLElement;
-
- const style = callEmbed.iframe.contentWindow?.getComputedStyle(el, '::before');
- if (!style) return;
- const tileBackgroundImage = style.getPropertyValue('background-image');
- const speaking = tileBackgroundImage !== 'none';
- if (!speaking) return;
-
- const speakerId = el.querySelector('[aria-label]')?.getAttribute('aria-label');
- if (speakerId && isUserId(speakerId)) {
- s.add(speakerId);
- }
- });
-
- setSpeakers(s);
- },
- [callEmbed]
- )
- );
-
- useEffect(() => {
- videoContainers?.forEach((element) => {
- mutationObserver.observe(element, {
- attributes: true,
- attributeFilter: ['class', 'style'],
- });
- });
-
- return () => {
- mutationObserver.disconnect();
- };
- }, [videoContainers, mutationObserver]);
-
- return speakers;
-};
diff --git a/src/app/hooks/useCommands.ts b/src/app/hooks/useCommands.ts
index 11f379dc..c02b0e94 100644
--- a/src/app/hooks/useCommands.ts
+++ b/src/app/hooks/useCommands.ts
@@ -15,6 +15,7 @@ import {
addRoomIdToMDirect,
getDMRoomFor,
guessDmRoomUserId,
+ isRoomAlias,
isRoomId,
isServerName,
isUserId,
@@ -25,7 +26,6 @@ import { useRoomNavigate } from './useRoomNavigate';
import { Membership, StateEvent } from '../../types/matrix/room';
import { getStateEvent } from '../utils/room';
import { splitWithSpace } from '../utils/common';
-import { runJoinCommand } from '../features/estate/joinEstateRoom';
import { createRoomEncryptionState } from '../components/create-room';
export const SHRUG = 'Β―\\_(γ)_/Β―';
@@ -227,7 +227,15 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
[Command.Join]: {
name: Command.Join,
description: 'Join room with address. Example: /join address1 address2',
- exe: (payload) => runJoinCommand(mx, payload, (message) => window.alert(message)),
+ exe: async (payload) => {
+ const rawIds = splitWithSpace(payload);
+ const roomIdOrAliases = rawIds.filter(
+ (idOrAlias) => isRoomId(idOrAlias) || isRoomAlias(idOrAlias)
+ );
+ roomIdOrAliases.forEach(async (idOrAlias) => {
+ await mx.joinRoom(idOrAlias);
+ });
+ },
},
[Command.Leave]: {
name: Command.Leave,
diff --git a/src/app/hooks/useDateFormat.ts b/src/app/hooks/useDateFormat.ts
index d2921140..520d4b0d 100644
--- a/src/app/hooks/useDateFormat.ts
+++ b/src/app/hooks/useDateFormat.ts
@@ -25,10 +25,6 @@ export const useDateFormatItems = (): DateFormatItem[] =>
format: 'YYYY/MM/DD',
name: 'YYYY/MM/DD',
},
- {
- format: 'YYYY-MM-DD',
- name: 'YYYY-MM-DD',
- },
{
format: '',
name: 'Custom',
diff --git a/src/app/hooks/useLivekitSupport.ts b/src/app/hooks/useLivekitSupport.ts
deleted file mode 100644
index 3cb2c1d8..00000000
--- a/src/app/hooks/useLivekitSupport.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { AutoDiscoveryInfo } from '../cs-api';
-import { useAutoDiscoveryInfo } from './useAutoDiscoveryInfo';
-
-export const livekitSupport = (autoDiscoveryInfo: AutoDiscoveryInfo): boolean => {
- const rtcFoci = autoDiscoveryInfo['org.matrix.msc4143.rtc_foci'];
-
- return (
- Array.isArray(rtcFoci) && rtcFoci.some((info) => typeof info.livekit_service_url === 'string')
- );
-};
-
-export const useLivekitSupport = (): boolean => {
- const autoDiscoveryInfo = useAutoDiscoveryInfo();
-
- return livekitSupport(autoDiscoveryInfo);
-};
diff --git a/src/app/hooks/useMemberSort.ts b/src/app/hooks/useMemberSort.ts
index 48d2f605..d8e403c5 100644
--- a/src/app/hooks/useMemberSort.ts
+++ b/src/app/hooks/useMemberSort.ts
@@ -47,10 +47,7 @@ export const useMemberSort = (index: number, memberSort: MemberSortItem[]): Memb
return item;
};
-export const useMemberPowerSort = (
- creators: Set,
- getPowerLevel: (userId: string) => number
-): MemberSortFn => {
+export const useMemberPowerSort = (creators: Set): MemberSortFn => {
const sort: MemberSortFn = useCallback(
(a, b) => {
if (creators.has(a.userId) && creators.has(b.userId)) {
@@ -59,7 +56,7 @@ export const useMemberPowerSort = (
if (creators.has(a.userId)) return -1;
if (creators.has(b.userId)) return 1;
- return getPowerLevel(b.userId) - getPowerLevel(a.userId);
+ return b.powerLevel - a.powerLevel;
},
[creators]
);
diff --git a/src/app/hooks/useMutationObserver.ts b/src/app/hooks/useMutationObserver.ts
deleted file mode 100644
index 5d0ce2ab..00000000
--- a/src/app/hooks/useMutationObserver.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useEffect, useMemo } from 'react';
-
-export type OnMutationCallback = (mutations: MutationRecord[]) => void;
-
-export const getMutationRecord = (
- target: Node,
- mutations: MutationRecord[]
-): MutationRecord | undefined => mutations.find((mutation) => mutation.target === target);
-
-export const useMutationObserver = (
- onMutationCallback: OnMutationCallback,
- observeElement?: Node | null | (() => Node | null),
- options?: MutationObserverInit
-): MutationObserver => {
- const mutationObserver = useMemo(
- () => new MutationObserver(onMutationCallback),
- [onMutationCallback]
- );
-
- useEffect(() => () => mutationObserver?.disconnect(), [mutationObserver]);
-
- useEffect(() => {
- const element = typeof observeElement === 'function' ? observeElement() : observeElement;
-
- if (element) {
- mutationObserver.observe(element, options);
- }
-
- return () => {
- if (element) {
- mutationObserver.disconnect();
- }
- };
- }, [mutationObserver, observeElement, options]);
-
- return mutationObserver;
-};
diff --git a/src/app/hooks/useMutualRooms.ts b/src/app/hooks/useMutualRooms.ts
index 28c5060b..a7b38938 100644
--- a/src/app/hooks/useMutualRooms.ts
+++ b/src/app/hooks/useMutualRooms.ts
@@ -1,10 +1,9 @@
import { useCallback } from 'react';
-import { MatrixClient, Method } from 'matrix-js-sdk';
import { useMatrixClient } from './useMatrixClient';
import { AsyncState, useAsyncCallbackValue } from './useAsyncCallback';
import { useSpecVersions } from './useSpecVersions';
-export const useUnstableMutualRoomsSupport = (): boolean => {
+export const useMutualRoomsSupport = (): boolean => {
const { unstable_features: unstableFeatures } = useSpecVersions();
const supported =
@@ -15,59 +14,16 @@ export const useUnstableMutualRoomsSupport = (): boolean => {
return !!supported;
};
-export const useMutualRoomsSupport = (): boolean => {
- const { unstable_features: unstableFeatures, versions } = useSpecVersions();
-
- const supported =
- versions.includes('v1.19') ||
- unstableFeatures?.['uk.half-shot.msc2666.query_mutual_rooms.stable'];
-
- return !!supported;
-};
-
-type MutualRoomsOK = {
- joined: string[];
- next_batch?: string;
- count: number;
-};
-
-const fetchAllMutualRooms = async (mx: MatrixClient, userId: string): Promise => {
- const mutualRooms: Set = new Set();
-
- let nextBatch: string | undefined;
- do {
- // eslint-disable-next-line no-await-in-loop
- const result = await mx.http.authedRequest(
- Method.Get,
- '/mutual_rooms',
- {
- user_id: userId,
- from: nextBatch,
- },
- undefined,
- {
- prefix: '/_matrix/client/v1',
- }
- );
- result.joined.forEach((r) => mutualRooms.add(r));
- nextBatch = result.next_batch;
- } while (typeof nextBatch === 'string');
-
- return Array.from(mutualRooms);
-};
-
export const useMutualRooms = (userId: string): AsyncState => {
const mx = useMatrixClient();
- const unstableSupport = useUnstableMutualRoomsSupport();
- const support = useMutualRoomsSupport();
+ const supported = useMutualRoomsSupport();
const [mutualRoomsState] = useAsyncCallbackValue(
- useCallback(() => {
- if (support) return fetchAllMutualRooms(mx, userId);
- if (unstableSupport) return mx._unstable_getSharedRooms(userId);
- return Promise.resolve([]);
- }, [mx, userId, unstableSupport, support])
+ useCallback(
+ () => (supported ? mx._unstable_getSharedRooms(userId) : Promise.resolve([])),
+ [mx, userId, supported]
+ )
);
return mutualRoomsState;
diff --git a/src/app/hooks/usePowerLevels.ts b/src/app/hooks/usePowerLevels.ts
index e6b754fa..0281b23c 100644
--- a/src/app/hooks/usePowerLevels.ts
+++ b/src/app/hooks/usePowerLevels.ts
@@ -57,7 +57,7 @@ const fillMissingPowers = (powerLevels: IPowerLevels): IPowerLevels =>
return draftPl;
});
-export const getPowersLevelFromMatrixEvent = (mEvent?: MatrixEvent): IPowerLevels => {
+const getPowersLevelFromMatrixEvent = (mEvent?: MatrixEvent): IPowerLevels => {
const plContent = mEvent?.getContent();
const powerLevels = !plContent ? DEFAULT_POWER_LEVELS : fillMissingPowers(plContent);
diff --git a/src/app/hooks/useRoomMeta.ts b/src/app/hooks/useRoomMeta.ts
index 086c3a56..8b0ae8cc 100644
--- a/src/app/hooks/useRoomMeta.ts
+++ b/src/app/hooks/useRoomMeta.ts
@@ -20,8 +20,6 @@ export const useRoomName = (room: Room): string => {
const [name, setName] = useState(room.name);
useEffect(() => {
- setName(room.name);
-
const handleRoomNameChange: RoomEventHandlerMap[RoomEvent.Name] = () => {
setName(room.name);
};
diff --git a/src/app/hooks/useRoomThreads.ts b/src/app/hooks/useRoomThreads.ts
new file mode 100644
index 00000000..7d5fa807
--- /dev/null
+++ b/src/app/hooks/useRoomThreads.ts
@@ -0,0 +1,20 @@
+import { useCallback } from 'react';
+import { EventTimelineSet, Room } from 'matrix-js-sdk';
+import { AsyncState, useAsyncCallbackValue } from './useAsyncCallback';
+
+export const useRoomMyThreads = (room: Room): AsyncState => {
+ const [threadsState] = useAsyncCallbackValue(
+ useCallback(async () => {
+ await room.createThreadsTimelineSets();
+ await room.fetchRoomThreads();
+
+ const timelineSet = room.threadsTimelineSets[0];
+ if (timelineSet === undefined) {
+ throw new Error('Failed to fetch My Threads!');
+ }
+ return timelineSet;
+ }, [room])
+ );
+
+ return threadsState;
+};
diff --git a/src/app/hooks/useSidebarItems.ts b/src/app/hooks/useSidebarItems.ts
new file mode 100644
index 00000000..16aba225
--- /dev/null
+++ b/src/app/hooks/useSidebarItems.ts
@@ -0,0 +1,138 @@
+import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
+import { MatrixClient } from 'matrix-js-sdk';
+import { AccountDataEvent } from '../../types/matrix/accountData';
+import { useMatrixClient } from './useMatrixClient';
+import { getAccountData, isSpace } from '../utils/room';
+import { Membership } from '../../types/matrix/room';
+import { useAccountDataCallback } from './useAccountDataCallback';
+
+export type ISidebarFolder = {
+ name?: string;
+ id: string;
+ content: string[];
+};
+export type TSidebarItem = string | ISidebarFolder;
+export type SidebarItems = Array;
+
+export type InCinnySpacesContent = {
+ shortcut?: string[];
+ sidebar?: SidebarItems;
+};
+
+export const parseSidebar = (
+ mx: MatrixClient,
+ orphanSpaces: string[],
+ content?: InCinnySpacesContent
+) => {
+ const sidebar = content?.sidebar ?? content?.shortcut ?? [];
+ const orphans = new Set(orphanSpaces);
+
+ const items: SidebarItems = [];
+
+ const safeToAdd = (spaceId: string): boolean => {
+ if (typeof spaceId !== 'string') return false;
+ const space = mx.getRoom(spaceId);
+ if (space?.getMyMembership() !== Membership.Join) return false;
+ return isSpace(space);
+ };
+
+ sidebar.forEach((item) => {
+ if (typeof item === 'string') {
+ if (safeToAdd(item) && !items.includes(item)) {
+ orphans.delete(item);
+ items.push(item);
+ }
+ return;
+ }
+ if (
+ typeof item === 'object' &&
+ typeof item.id === 'string' &&
+ Array.isArray(item.content) &&
+ !items.find((i) => (typeof i === 'string' ? false : i.id === item.id))
+ ) {
+ const safeContent = item.content.filter(safeToAdd);
+ safeContent.forEach((i) => orphans.delete(i));
+ items.push({
+ ...item,
+ content: Array.from(new Set(safeContent)),
+ });
+ }
+ });
+
+ orphans.forEach((spaceId) => items.push(spaceId));
+ return items;
+};
+
+export const useSidebarItems = (
+ orphanSpaces: string[]
+): [SidebarItems, Dispatch>] => {
+ const mx = useMatrixClient();
+
+ const [sidebarItems, setSidebarItems] = useState(() => {
+ const inCinnySpacesContent = getAccountData(
+ mx,
+ AccountDataEvent.CinnySpaces
+ )?.getContent();
+ return parseSidebar(mx, orphanSpaces, inCinnySpacesContent);
+ });
+
+ useEffect(() => {
+ const inCinnySpacesContent = getAccountData(
+ mx,
+ AccountDataEvent.CinnySpaces
+ )?.getContent();
+ setSidebarItems(parseSidebar(mx, orphanSpaces, inCinnySpacesContent));
+ }, [mx, orphanSpaces]);
+
+ useAccountDataCallback(
+ mx,
+ useCallback(
+ (mEvent) => {
+ if (mEvent.getType() === AccountDataEvent.CinnySpaces) {
+ const newContent = mEvent.getContent();
+ setSidebarItems(parseSidebar(mx, orphanSpaces, newContent));
+ }
+ },
+ [mx, orphanSpaces]
+ )
+ );
+
+ return [sidebarItems, setSidebarItems];
+};
+
+export const sidebarItemWithout = (items: SidebarItems, roomId: string) => {
+ const newItems: SidebarItems = items
+ .map((item) => {
+ if (typeof item === 'string') {
+ if (item === roomId) return null;
+ return item;
+ }
+ if (item.content.includes(roomId)) {
+ const newContent = item.content.filter((id) => id !== roomId);
+ if (newContent.length === 0) return null;
+ return {
+ ...item,
+ content: newContent,
+ };
+ }
+ return item;
+ })
+ .filter((item) => item !== null) as SidebarItems;
+
+ return newItems;
+};
+
+export const makeCinnySpacesContent = (
+ mx: MatrixClient,
+ items: SidebarItems
+): InCinnySpacesContent => {
+ const currentInSpaces =
+ getAccountData(mx, AccountDataEvent.CinnySpaces)?.getContent() ?? {};
+
+ const newSpacesContent: InCinnySpacesContent = {
+ ...currentInSpaces,
+ sidebar: items,
+ };
+
+ return newSpacesContent;
+};
diff --git a/src/app/hooks/useSpaceHierarchy.ts b/src/app/hooks/useSpaceHierarchy.ts
index 59889c1c..ad34e3f4 100644
--- a/src/app/hooks/useSpaceHierarchy.ts
+++ b/src/app/hooks/useSpaceHierarchy.ts
@@ -276,8 +276,7 @@ export type FetchSpaceHierarchyLevelData = {
};
export const useFetchSpaceHierarchyLevel = (
roomId: string,
- enable: boolean,
- maxDepth = 1
+ enable: boolean
): FetchSpaceHierarchyLevelData => {
const mx = useMatrixClient();
const pageNoRef = useRef(0);
@@ -287,13 +286,13 @@ export const useFetchSpaceHierarchyLevel = (
string[],
string | undefined
> = useCallback(
- ({ pageParam }) => mx.getRoomHierarchy(roomId, PER_PAGE_COUNT, maxDepth, false, pageParam),
- [roomId, maxDepth, mx]
+ ({ pageParam }) => mx.getRoomHierarchy(roomId, PER_PAGE_COUNT, 1, false, pageParam),
+ [roomId, mx]
);
const queryResponse = useInfiniteQuery({
refetchOnMount: enable,
- queryKey: [roomId, 'hierarchy_level', maxDepth],
+ queryKey: [roomId, 'hierarchy_level'],
initialPageParam: undefined,
queryFn: fetchLevel,
getNextPageParam: (result) => {
diff --git a/src/app/hooks/useStateEvents.ts b/src/app/hooks/useStateEvents.ts
new file mode 100644
index 00000000..dd085693
--- /dev/null
+++ b/src/app/hooks/useStateEvents.ts
@@ -0,0 +1,28 @@
+import { useCallback, useMemo } from 'react';
+import { Room } from 'matrix-js-sdk';
+import { StateEvent } from '../../types/matrix/room';
+import { useForceUpdate } from './useForceUpdate';
+import { useStateEventCallback } from './useStateEventCallback';
+import { getStateEvents } from '../utils/room';
+
+export const useStateEvents = (room: Room, eventType: StateEvent) => {
+ const [updateCount, forceUpdate] = useForceUpdate();
+
+ useStateEventCallback(
+ room.client,
+ useCallback(
+ (event) => {
+ if (event.getRoomId() === room.roomId && event.getType() === eventType) {
+ forceUpdate();
+ }
+ },
+ [room, eventType, forceUpdate]
+ )
+ );
+
+ return useMemo(
+ () => getStateEvents(room, eventType),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [room, eventType, updateCount]
+ );
+};
diff --git a/src/app/hooks/useTheme.ts b/src/app/hooks/useTheme.ts
index 4bfe01da..cdbb9dba 100644
--- a/src/app/hooks/useTheme.ts
+++ b/src/app/hooks/useTheme.ts
@@ -1,8 +1,7 @@
import { lightTheme } from 'folds';
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
-import { butterTheme, darkTheme, gamecultTheme, silverTheme } from '../../colors.css';
-import '../../gamecult.css';
+import { butterTheme, darkTheme, silverTheme } from '../../colors.css';
import { settingsAtom } from '../state/settings';
import { useSetting } from '../state/hooks/settings';
@@ -39,14 +38,8 @@ export const ButterTheme: Theme = {
classNames: ['butter-theme', butterTheme, onDarkFontWeight, 'prism-dark'],
};
-export const GameCultTheme: Theme = {
- id: 'gamecult-theme',
- kind: ThemeKind.Dark,
- classNames: ['gamecult-theme', gamecultTheme, onDarkFontWeight, 'prism-dark'],
-};
-
export const useThemes = (): Theme[] => {
- const themes: Theme[] = useMemo(() => [GameCultTheme, LightTheme, SilverTheme, DarkTheme, ButterTheme], []);
+ const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme], []);
return themes;
};
@@ -54,7 +47,6 @@ export const useThemes = (): Theme[] => {
export const useThemeNames = (): Record =>
useMemo(
() => ({
- [GameCultTheme.id]: 'GameCult',
[LightTheme.id]: 'Light',
[SilverTheme.id]: 'Silver',
[DarkTheme.id]: 'Dark',
@@ -92,15 +84,15 @@ export const useActiveTheme = (): Theme => {
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
if (!systemTheme) {
- const selectedTheme = themes.find((theme) => theme.id === themeId) ?? GameCultTheme;
+ const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
return selectedTheme;
}
const selectedTheme =
systemThemeKind === ThemeKind.Dark
- ? themes.find((theme) => theme.id === darkThemeId) ?? GameCultTheme
- : themes.find((theme) => theme.id === lightThemeId) ?? GameCultTheme;
+ ? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
+ : themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
return selectedTheme;
};
diff --git a/src/app/pages/CallStatusRenderer.tsx b/src/app/pages/CallStatusRenderer.tsx
deleted file mode 100644
index 2836886e..00000000
--- a/src/app/pages/CallStatusRenderer.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-import { useCallEmbed } from '../hooks/useCallEmbed';
-import { CallStatus } from '../features/call-status';
-import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
-import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
-
-export function CallStatusRenderer() {
- const callEmbed = useCallEmbed();
- const selectedRoom = useSelectedRoom();
-
- const screenSize = useScreenSizeContext();
-
- if (!callEmbed) return null;
-
- if (screenSize === ScreenSize.Mobile && callEmbed.roomId === selectedRoom) return null;
-
- return ;
-}
diff --git a/src/app/pages/MobileFriendly.tsx b/src/app/pages/MobileFriendly.tsx
index 225b1b80..ca947ac6 100644
--- a/src/app/pages/MobileFriendly.tsx
+++ b/src/app/pages/MobileFriendly.tsx
@@ -1,6 +1,28 @@
import { ReactNode } from 'react';
import { useMatch } from 'react-router-dom';
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
+import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from './paths';
+
+type MobileFriendlyClientNavProps = {
+ children: ReactNode;
+};
+export function MobileFriendlyClientNav({ children }: MobileFriendlyClientNavProps) {
+ const screenSize = useScreenSizeContext();
+ const homeMatch = useMatch({ path: HOME_PATH, caseSensitive: true, end: true });
+ const directMatch = useMatch({ path: DIRECT_PATH, caseSensitive: true, end: true });
+ const spaceMatch = useMatch({ path: SPACE_PATH, caseSensitive: true, end: true });
+ const exploreMatch = useMatch({ path: EXPLORE_PATH, caseSensitive: true, end: true });
+ const inboxMatch = useMatch({ path: INBOX_PATH, caseSensitive: true, end: true });
+
+ if (
+ screenSize === ScreenSize.Mobile &&
+ !(homeMatch || directMatch || spaceMatch || exploreMatch || inboxMatch)
+ ) {
+ return null;
+ }
+
+ return children;
+}
type MobileFriendlyPageNavProps = {
path: string;
diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx
index 159162dd..18591f29 100644
--- a/src/app/pages/Router.tsx
+++ b/src/app/pages/Router.tsx
@@ -40,19 +40,19 @@ import {
getSpaceLobbyPath,
} from './pathUtils';
import { ClientBindAtoms, ClientLayout, ClientRoot } from './client';
-import { HomeRouteRoomProvider, HomeSearch } from './client/home';
-import { DirectCreate, DirectRouteRoomProvider } from './client/direct';
-import { RouteSpaceProvider, SpaceRouteRoomProvider, SpaceSearch } from './client/space';
+import { Home, HomeRouteRoomProvider, HomeSearch } from './client/home';
+import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct';
+import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space';
import { Explore, FeaturedRooms, PublicRooms } from './client/explore';
import { Notifications, Inbox, Invites } from './client/inbox';
import { setAfterLoginRedirectPath } from './afterLoginRedirectPath';
import { Room } from '../features/room';
import { Lobby } from '../features/lobby';
import { WelcomePage } from './client/WelcomePage';
-import { EstatePageNav } from './client/EstatePageNav';
+import { SidebarNav } from './client/SidebarNav';
import { PageRoot } from '../components/page';
import { ScreenSize } from '../hooks/useScreenSize';
-import { MobileFriendlyPageNav } from './MobileFriendly';
+import { MobileFriendlyPageNav, MobileFriendlyClientNav } from './MobileFriendly';
import { ClientInitStorageAtom } from './client/ClientInitStorageAtom';
import { ClientNonUIFeatures } from './client/ClientNonUIFeatures';
import { AuthRouteThemeManager, UnAuthRouteThemeManager } from './ThemeManager';
@@ -68,8 +68,6 @@ import { Create } from './client/create';
import { CreateSpaceModalRenderer } from '../features/create-space';
import { SearchModalRenderer } from '../features/search';
import { getFallbackSession } from '../state/sessions';
-import { CallStatusRenderer } from './CallStatusRenderer';
-import { CallEmbedProvider } from '../components/CallEmbedProvider';
export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
const { hashRouter } = clientConfig;
@@ -108,8 +106,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
{
- const session = getFallbackSession();
- if (!session) {
+ if (!getFallbackSession()) {
const afterLoginPath = getAppPathFromHref(
getOriginBaseUrl(hashRouter),
window.location.href
@@ -126,12 +123,15 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
-
-
-
-
-
-
+
+
+
+ }
+ >
+
+
@@ -151,7 +151,13 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
}>
+
+
+
+ }
+ >
}
@@ -172,7 +178,13 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
}>
+
+
+
+ }
+ >
}
@@ -192,7 +204,13 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
path={SPACE_PATH}
element={
- }>
+
+
+
+ }
+ >
diff --git a/src/app/pages/ThemeManager.tsx b/src/app/pages/ThemeManager.tsx
index 983ca684..69d50cdb 100644
--- a/src/app/pages/ThemeManager.tsx
+++ b/src/app/pages/ThemeManager.tsx
@@ -1,20 +1,29 @@
import React, { ReactNode, useEffect } from 'react';
import { configClass, varsClass } from 'folds';
import {
- GameCultTheme,
+ DarkTheme,
+ LightTheme,
ThemeContextProvider,
+ ThemeKind,
useActiveTheme,
+ useSystemThemeKind,
} from '../hooks/useTheme';
import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings';
export function UnAuthRouteThemeManager() {
+ const systemThemeKind = useSystemThemeKind();
+
useEffect(() => {
document.body.className = '';
document.body.classList.add(configClass, varsClass);
- // The GameCult identity is single-theme: signed-out pages ignore the system scheme.
- document.body.classList.add(...GameCultTheme.classNames);
- }, []);
+ if (systemThemeKind === ThemeKind.Dark) {
+ document.body.classList.add(...DarkTheme.classNames);
+ }
+ if (systemThemeKind === ThemeKind.Light) {
+ document.body.classList.add(...LightTheme.classNames);
+ }
+ }, [systemThemeKind]);
return null;
}
diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx
index b0b94319..25c70e6e 100644
--- a/src/app/pages/auth/AuthFooter.tsx
+++ b/src/app/pages/auth/AuthFooter.tsx
@@ -15,7 +15,7 @@ export function AuthFooter() {
target="_blank"
rel="noreferrer"
>
- v4.12.8
+ v4.10.0
Twitter
diff --git a/src/app/pages/client/AutoDiscovery.tsx b/src/app/pages/client/AutoDiscovery.tsx
deleted file mode 100644
index 76423477..00000000
--- a/src/app/pages/client/AutoDiscovery.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React, { ReactNode, useCallback, useMemo } from 'react';
-import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo';
-import { AsyncStatus, useAsyncCallbackValue } from '../../hooks/useAsyncCallback';
-import { autoDiscovery, AutoDiscoveryInfo } from '../../cs-api';
-import { getMxIdServer } from '../../utils/matrix';
-
-type AutoDiscoveryProps = {
- userId: string;
- baseUrl: string;
- children: ReactNode;
-};
-export function AutoDiscovery({ userId, baseUrl, children }: AutoDiscoveryProps) {
- const [state] = useAsyncCallbackValue(
- useCallback(async () => {
- const server = getMxIdServer(userId);
- return autoDiscovery(fetch, server ?? userId);
- }, [userId])
- );
-
- const [, info] = state.status === AsyncStatus.Success ? state.data : [];
-
- const fallback: AutoDiscoveryInfo = useMemo(
- () => ({
- 'm.homeserver': {
- base_url: baseUrl,
- },
- }),
- [baseUrl]
- );
-
- return {children} ;
-}
diff --git a/src/app/pages/client/ClientInitStorageAtom.tsx b/src/app/pages/client/ClientInitStorageAtom.tsx
index 5793bb6b..308a0212 100644
--- a/src/app/pages/client/ClientInitStorageAtom.tsx
+++ b/src/app/pages/client/ClientInitStorageAtom.tsx
@@ -8,8 +8,8 @@ import { makeNavToActivePathAtom } from '../../state/navToActivePath';
import { NavToActivePathProvider } from '../../state/hooks/navToActivePath';
import { makeOpenedSidebarFolderAtom } from '../../state/openedSidebarFolder';
import { OpenedSidebarFolderProvider } from '../../state/hooks/openedSidebarFolder';
-import { makeCallPreferencesAtom } from '../../state/callPreferences';
-import { CallPreferencesProvider } from '../../state/hooks/callPreferences';
+import { makeRoomToActiveThreadAtom } from '../../state/roomToActiveThread';
+import { RoomToActiveThreadProvider } from '../../state/hooks/roomToActiveThread';
type ClientInitStorageAtomProps = {
children: ReactNode;
@@ -24,19 +24,19 @@ export function ClientInitStorageAtom({ children }: ClientInitStorageAtomProps)
const navToActivePathAtom = useMemo(() => makeNavToActivePathAtom(userId), [userId]);
- const openedSidebarFolderAtom = useMemo(() => makeOpenedSidebarFolderAtom(userId), [userId]);
+ const roomToActiveThreadAtom = useMemo(() => makeRoomToActiveThreadAtom(userId), [userId]);
- const callPreferencesAtom = useMemo(() => makeCallPreferencesAtom(userId), [userId]);
+ const openedSidebarFolderAtom = useMemo(() => makeOpenedSidebarFolderAtom(userId), [userId]);
return (
-
-
+
+
{children}
-
-
+
+
diff --git a/src/app/pages/client/ClientLayout.tsx b/src/app/pages/client/ClientLayout.tsx
index e1d83c95..4a077ba6 100644
--- a/src/app/pages/client/ClientLayout.tsx
+++ b/src/app/pages/client/ClientLayout.tsx
@@ -1,6 +1,15 @@
import React, { ReactNode } from 'react';
import { Box } from 'folds';
-export function ClientLayout({ children }: { children: ReactNode }) {
- return {children} ;
+type ClientLayoutProps = {
+ nav: ReactNode;
+ children: ReactNode;
+};
+export function ClientLayout({ nav, children }: ClientLayoutProps) {
+ return (
+
+ {nav}
+ {children}
+
+ );
}
diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx
index 93f0526e..e1a5dc0c 100644
--- a/src/app/pages/client/ClientRoot.tsx
+++ b/src/app/pages/client/ClientRoot.tsx
@@ -35,7 +35,6 @@ import { stopPropagation } from '../../utils/keyboard';
import { SyncStatus } from './SyncStatus';
import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
import { getFallbackSession } from '../../state/sessions';
-import { AutoDiscovery } from './AutoDiscovery';
function ClientRootLoading() {
return (
@@ -144,7 +143,7 @@ type ClientRootProps = {
};
export function ClientRoot({ children }: ClientRootProps) {
const [loading, setLoading] = useState(true);
- const { baseUrl, userId } = getFallbackSession() ?? {};
+ const { baseUrl } = getFallbackSession() ?? {};
const [loadState, loadMatrix] = useAsyncCallback(
useCallback(() => {
@@ -184,55 +183,47 @@ export function ClientRoot({ children }: ClientRootProps) {
);
return (
-
-
- {mx && }
- {loading && }
- {(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && (
-
-
-
-
- {loadState.status === AsyncStatus.Error && (
- {`Failed to load. ${loadState.error.message}`}
- )}
- {startState.status === AsyncStatus.Error && (
- {`Failed to start. ${startState.error.message}`}
- )}
- startMatrix(mx) : loadMatrix}>
-
- Retry
-
-
-
-
-
-
- )}
- {loading || !mx ? (
-
- ) : (
-
-
- {(serverConfigs) => (
-
-
-
- {children}
-
-
-
- )}
-
-
- )}
-
-
+
+ {mx && }
+ {loading && }
+ {(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && (
+
+
+
+
+ {loadState.status === AsyncStatus.Error && (
+ {`Failed to load. ${loadState.error.message}`}
+ )}
+ {startState.status === AsyncStatus.Error && (
+ {`Failed to start. ${startState.error.message}`}
+ )}
+ startMatrix(mx) : loadMatrix}>
+
+ Retry
+
+
+
+
+
+
+ )}
+ {loading || !mx ? (
+
+ ) : (
+
+
+ {(serverConfigs) => (
+
+
+
+ {children}
+
+
+
+ )}
+
+
+ )}
+
);
}
diff --git a/src/app/pages/client/EstatePageNav.tsx b/src/app/pages/client/EstatePageNav.tsx
deleted file mode 100644
index 470cbdd7..00000000
--- a/src/app/pages/client/EstatePageNav.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react';
-import { MobileFriendlyPageNav } from '../MobileFriendly';
-import { EstateNavigator } from '../../features/estate';
-
-/**
- * Where the navigator mounts in a route's nav slot. The wide-screen placement
- * question (ratatoskr:question:desktop-navigator-placement) is answered here.
- */
-export function EstatePageNav({ path }: { path: string }) {
- return (
-
-
-
- );
-}
diff --git a/src/app/pages/client/SidebarNav.tsx b/src/app/pages/client/SidebarNav.tsx
new file mode 100644
index 00000000..a2e1b682
--- /dev/null
+++ b/src/app/pages/client/SidebarNav.tsx
@@ -0,0 +1,56 @@
+import React, { useRef } from 'react';
+import { Scroll } from 'folds';
+
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarStackSeparator,
+ SidebarStack,
+} from '../../components/sidebar';
+import {
+ DirectTab,
+ HomeTab,
+ SpaceTabs,
+ InboxTab,
+ ExploreTab,
+ SettingsTab,
+ UnverifiedTab,
+ SearchTab,
+} from './sidebar';
+import { CreateTab } from './sidebar/CreateTab';
+
+export function SidebarNav() {
+ const scrollRef = useRef(null);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ sticky={
+ <>
+
+
+
+
+
+
+
+ >
+ }
+ />
+
+ );
+}
diff --git a/src/app/pages/client/SpecVersions.tsx b/src/app/pages/client/SpecVersions.tsx
index 84eecf7d..952885cd 100644
--- a/src/app/pages/client/SpecVersions.tsx
+++ b/src/app/pages/client/SpecVersions.tsx
@@ -22,7 +22,7 @@ export function SpecVersions({ baseUrl, children }: { baseUrl: string; children:
- Unable to connect to the homeserver. The homeserver or your internet connection may be down.
+ Failed to connect to homeserver. Either homeserver is down or your internet.
diff --git a/src/app/pages/client/WelcomePage.tsx b/src/app/pages/client/WelcomePage.tsx
index 6a25392c..37ca10fa 100644
--- a/src/app/pages/client/WelcomePage.tsx
+++ b/src/app/pages/client/WelcomePage.tsx
@@ -24,7 +24,7 @@ export function WelcomePage() {
target="_blank"
rel="noreferrer noopener"
>
- v4.12.8
+ v4.10.0
}
diff --git a/src/app/pages/client/direct/Direct.tsx b/src/app/pages/client/direct/Direct.tsx
new file mode 100644
index 00000000..04edf109
--- /dev/null
+++ b/src/app/pages/client/direct/Direct.tsx
@@ -0,0 +1,280 @@
+import React, { MouseEventHandler, forwardRef, useMemo, useRef, useState } from 'react';
+import { useAtom, useAtomValue } from 'jotai';
+import {
+ Avatar,
+ Box,
+ Button,
+ Icon,
+ IconButton,
+ Icons,
+ Menu,
+ MenuItem,
+ PopOut,
+ RectCords,
+ Text,
+ config,
+ toRem,
+} from 'folds';
+import { useVirtualizer } from '@tanstack/react-virtual';
+import FocusTrap from 'focus-trap-react';
+import { useNavigate } from 'react-router-dom';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { factoryRoomIdByActivity } from '../../../utils/sort';
+import {
+ NavButton,
+ NavCategory,
+ NavCategoryHeader,
+ NavEmptyCenter,
+ NavEmptyLayout,
+ NavItem,
+ NavItemContent,
+} from '../../../components/nav';
+import { getDirectCreatePath, getDirectRoomPath } from '../../pathUtils';
+import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
+import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
+import { VirtualTile } from '../../../components/virtualizer';
+import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
+import { makeNavCategoryId } from '../../../state/closedNavCategories';
+import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
+import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
+import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
+import { useDirectRooms } from './useDirectRooms';
+import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
+import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
+import { useRoomsUnread } from '../../../state/hooks/unread';
+import { markAsRead } from '../../../utils/notifications';
+import { stopPropagation } from '../../../utils/keyboard';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
+import {
+ getRoomNotificationMode,
+ useRoomsNotificationPreferencesContext,
+} from '../../../hooks/useRoomsNotificationPreferences';
+import { useDirectCreateSelected } from '../../../hooks/router/useDirectSelected';
+
+type DirectMenuProps = {
+ requestClose: () => void;
+};
+const DirectMenu = forwardRef(({ requestClose }, ref) => {
+ const mx = useMatrixClient();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const orphanRooms = useDirectRooms();
+ const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
+
+ const handleMarkAsRead = () => {
+ if (!unread) return;
+ orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
+ requestClose();
+ };
+
+ return (
+
+
+ }
+ radii="300"
+ aria-disabled={!unread}
+ >
+
+ Mark as Read
+
+
+
+
+ );
+});
+
+function DirectHeader() {
+ const [menuAnchor, setMenuAnchor] = useState();
+
+ const handleOpenMenu: MouseEventHandler = (evt) => {
+ const cords = evt.currentTarget.getBoundingClientRect();
+ setMenuAnchor((currentState) => {
+ if (currentState) return undefined;
+ return cords;
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+ Direct Messages
+
+
+
+
+
+
+
+
+
+ setMenuAnchor(undefined),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
+ isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+ setMenuAnchor(undefined)} />
+
+ }
+ />
+ >
+ );
+}
+
+function DirectEmpty() {
+ const navigate = useNavigate();
+
+ return (
+
+ }
+ title={
+
+ No Direct Messages
+
+ }
+ content={
+
+ You do not have any direct messages yet.
+
+ }
+ options={
+ navigate(getDirectCreatePath())}>
+
+ Direct Message
+
+
+ }
+ />
+
+ );
+}
+
+const DEFAULT_CATEGORY_ID = makeNavCategoryId('direct', 'direct');
+export function Direct() {
+ const mx = useMatrixClient();
+ useNavToActivePathMapper('direct');
+ const scrollRef = useRef(null);
+ const directs = useDirectRooms();
+ const notificationPreferences = useRoomsNotificationPreferencesContext();
+ const roomToUnread = useAtomValue(roomToUnreadAtom);
+ const navigate = useNavigate();
+
+ const createDirectSelected = useDirectCreateSelected();
+
+ const selectedRoomId = useSelectedRoom();
+ const noRoomToDisplay = directs.length === 0;
+ const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom());
+
+ const sortedDirects = useMemo(() => {
+ const items = Array.from(directs).sort(factoryRoomIdByActivity(mx));
+ if (closedCategories.has(DEFAULT_CATEGORY_ID)) {
+ return items.filter((rId) => roomToUnread.has(rId) || rId === selectedRoomId);
+ }
+ return items;
+ }, [mx, directs, closedCategories, roomToUnread, selectedRoomId]);
+
+ const virtualizer = useVirtualizer({
+ count: sortedDirects.length,
+ getScrollElement: () => scrollRef.current,
+ estimateSize: () => 38,
+ overscan: 10,
+ });
+
+ const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) =>
+ closedCategories.has(categoryId)
+ );
+
+ return (
+
+
+ {noRoomToDisplay ? (
+
+ ) : (
+
+
+
+
+ navigate(getDirectCreatePath())}>
+
+
+
+
+
+
+
+ Create Chat
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ {virtualizer.getVirtualItems().map((vItem) => {
+ const roomId = sortedDirects[vItem.index];
+ const room = mx.getRoom(roomId);
+ if (!room) return null;
+ const selected = selectedRoomId === roomId;
+
+ return (
+
+
+
+ );
+ })}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/pages/client/direct/index.ts b/src/app/pages/client/direct/index.ts
index aaa8c929..d247bbc0 100644
--- a/src/app/pages/client/direct/index.ts
+++ b/src/app/pages/client/direct/index.ts
@@ -1,2 +1,3 @@
+export * from './Direct';
export * from './RoomProvider';
export * from './DirectCreate';
diff --git a/src/app/pages/client/home/Home.tsx b/src/app/pages/client/home/Home.tsx
new file mode 100644
index 00000000..d87db963
--- /dev/null
+++ b/src/app/pages/client/home/Home.tsx
@@ -0,0 +1,362 @@
+import React, { MouseEventHandler, forwardRef, useMemo, useRef, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import {
+ Avatar,
+ Box,
+ Button,
+ Icon,
+ IconButton,
+ Icons,
+ Menu,
+ MenuItem,
+ PopOut,
+ RectCords,
+ Text,
+ config,
+ toRem,
+} from 'folds';
+import { useVirtualizer } from '@tanstack/react-virtual';
+import { useAtom, useAtomValue } from 'jotai';
+import FocusTrap from 'focus-trap-react';
+import { factoryRoomIdByActivity, factoryRoomIdByAtoZ } from '../../../utils/sort';
+import {
+ NavButton,
+ NavCategory,
+ NavCategoryHeader,
+ NavEmptyCenter,
+ NavEmptyLayout,
+ NavItem,
+ NavItemContent,
+ NavLink,
+} from '../../../components/nav';
+import {
+ encodeSearchParamValueArray,
+ getExplorePath,
+ getHomeCreatePath,
+ getHomeRoomPath,
+ getHomeSearchPath,
+ withSearchParam,
+} from '../../pathUtils';
+import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
+import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
+import {
+ useHomeCreateSelected,
+ useHomeSearchSelected,
+} from '../../../hooks/router/useHomeSelected';
+import { useHomeRooms } from './useHomeRooms';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { VirtualTile } from '../../../components/virtualizer';
+import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
+import { makeNavCategoryId } from '../../../state/closedNavCategories';
+import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
+import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
+import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
+import { PageNav, PageNavHeader, PageNavContent } from '../../../components/page';
+import { useRoomsUnread } from '../../../state/hooks/unread';
+import { markAsRead } from '../../../utils/notifications';
+import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
+import { stopPropagation } from '../../../utils/keyboard';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
+import {
+ getRoomNotificationMode,
+ useRoomsNotificationPreferencesContext,
+} from '../../../hooks/useRoomsNotificationPreferences';
+import { UseStateProvider } from '../../../components/UseStateProvider';
+import { JoinAddressPrompt } from '../../../components/join-address-prompt';
+import { _RoomSearchParams } from '../../paths';
+
+type HomeMenuProps = {
+ requestClose: () => void;
+};
+const HomeMenu = forwardRef(({ requestClose }, ref) => {
+ const orphanRooms = useHomeRooms();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
+ const mx = useMatrixClient();
+
+ const handleMarkAsRead = () => {
+ if (!unread) return;
+ orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
+ requestClose();
+ };
+
+ return (
+
+
+ }
+ radii="300"
+ aria-disabled={!unread}
+ >
+
+ Mark as Read
+
+
+
+
+ );
+});
+
+function HomeHeader() {
+ const [menuAnchor, setMenuAnchor] = useState();
+
+ const handleOpenMenu: MouseEventHandler = (evt) => {
+ const cords = evt.currentTarget.getBoundingClientRect();
+ setMenuAnchor((currentState) => {
+ if (currentState) return undefined;
+ return cords;
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+ setMenuAnchor(undefined),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
+ isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+ setMenuAnchor(undefined)} />
+
+ }
+ />
+ >
+ );
+}
+
+function HomeEmpty() {
+ const navigate = useNavigate();
+
+ return (
+
+ }
+ title={
+
+ No Rooms
+
+ }
+ content={
+
+ You do not have any rooms yet.
+
+ }
+ options={
+ <>
+ navigate(getHomeCreatePath())} variant="Secondary" size="300">
+
+ Create Room
+
+
+ navigate(getExplorePath())}
+ variant="Secondary"
+ fill="Soft"
+ size="300"
+ >
+
+ Explore Community Rooms
+
+
+ >
+ }
+ />
+
+ );
+}
+
+const DEFAULT_CATEGORY_ID = makeNavCategoryId('home', 'room');
+export function Home() {
+ const mx = useMatrixClient();
+ useNavToActivePathMapper('home');
+ const scrollRef = useRef(null);
+ const rooms = useHomeRooms();
+ const notificationPreferences = useRoomsNotificationPreferencesContext();
+ const roomToUnread = useAtomValue(roomToUnreadAtom);
+ const navigate = useNavigate();
+
+ const selectedRoomId = useSelectedRoom();
+ const createRoomSelected = useHomeCreateSelected();
+ const searchSelected = useHomeSearchSelected();
+ const noRoomToDisplay = rooms.length === 0;
+ const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom());
+
+ const sortedRooms = useMemo(() => {
+ const items = Array.from(rooms).sort(
+ closedCategories.has(DEFAULT_CATEGORY_ID)
+ ? factoryRoomIdByActivity(mx)
+ : factoryRoomIdByAtoZ(mx)
+ );
+ if (closedCategories.has(DEFAULT_CATEGORY_ID)) {
+ return items.filter((rId) => roomToUnread.has(rId) || rId === selectedRoomId);
+ }
+ return items;
+ }, [mx, rooms, closedCategories, roomToUnread, selectedRoomId]);
+
+ const virtualizer = useVirtualizer({
+ count: sortedRooms.length,
+ getScrollElement: () => scrollRef.current,
+ estimateSize: () => 38,
+ overscan: 10,
+ });
+
+ const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) =>
+ closedCategories.has(categoryId)
+ );
+
+ return (
+
+
+ {noRoomToDisplay ? (
+
+ ) : (
+
+
+
+
+ navigate(getHomeCreatePath())}>
+
+
+
+
+
+
+
+ Create Room
+
+
+
+
+
+
+
+ {(open, setOpen) => (
+ <>
+
+ setOpen(true)}>
+
+
+
+
+
+
+
+ Join with Address
+
+
+
+
+
+
+ {open && (
+ setOpen(false)}
+ onOpen={(roomIdOrAlias, viaServers, eventId) => {
+ setOpen(false);
+ const path = getHomeRoomPath(roomIdOrAlias, eventId);
+ navigate(
+ viaServers
+ ? withSearchParam<_RoomSearchParams>(path, {
+ viaServers: encodeSearchParamValueArray(viaServers),
+ })
+ : path
+ );
+ }}
+ />
+ )}
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+ Message Search
+
+
+
+
+
+
+
+
+
+
+ Rooms
+
+
+
+ {virtualizer.getVirtualItems().map((vItem) => {
+ const roomId = sortedRooms[vItem.index];
+ const room = mx.getRoom(roomId);
+ if (!room) return null;
+ const selected = selectedRoomId === roomId;
+
+ return (
+
+
+
+ );
+ })}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/pages/client/home/index.ts b/src/app/pages/client/home/index.ts
index 5669f658..80764481 100644
--- a/src/app/pages/client/home/index.ts
+++ b/src/app/pages/client/home/index.ts
@@ -1,2 +1,3 @@
+export * from './Home';
export * from './Search';
export * from './RoomProvider';
diff --git a/src/app/pages/client/inbox/Invites.tsx b/src/app/pages/client/inbox/Invites.tsx
index ca26bac3..20518e56 100644
--- a/src/app/pages/client/inbox/Invites.tsx
+++ b/src/app/pages/client/inbox/Invites.tsx
@@ -45,8 +45,12 @@ import {
} from '../../../utils/room';
import { nameInitials } from '../../../utils/common';
import { RoomAvatar } from '../../../components/room-avatar';
-import { getMxIdLocalPart, rateLimitedActions } from '../../../utils/matrix';
-import { activateEstateRoom } from '../../../features/estate/joinEstateRoom';
+import {
+ addRoomIdToMDirect,
+ getMxIdLocalPart,
+ guessDmRoomUserId,
+ rateLimitedActions,
+} from '../../../utils/matrix';
import { Time } from '../../../components/message';
import { useElementSizeObserver } from '../../../hooks/useElementSizeObserver';
import { onEnterOrSpace, stopPropagation } from '../../../utils/keyboard';
@@ -156,20 +160,24 @@ function InviteCard({
hideAvatar,
}: InviteCardProps) {
const mx = useMatrixClient();
+ const userId = mx.getSafeUserId();
+
const [viewTopic, setViewTopic] = useState(false);
const closeTopic = () => setViewTopic(false);
const openTopic = () => setViewTopic(true);
const [joinState, join] = useAsyncCallback(
useCallback(async () => {
- const result = await activateEstateRoom(mx, {
- roomId: invite.roomId,
- state: 'invited',
- via: [],
- });
- if (result.kind === 'refused') throw new Error(result.message);
+ const dmUserId = isDirectInvite(invite.room, userId)
+ ? guessDmRoomUserId(invite.room, userId)
+ : undefined;
+
+ await mx.joinRoom(invite.roomId);
+ if (dmUserId) {
+ await addRoomIdToMDirect(mx, invite.roomId, dmUserId);
+ }
onNavigate(invite.roomId, invite.isSpace);
- }, [mx, invite, onNavigate])
+ }, [mx, invite, userId, onNavigate])
);
const [leaveState, leave] = useAsyncCallback, MatrixError, []>(
useCallback(() => mx.leave(invite.roomId), [mx, invite])
diff --git a/src/app/pages/client/inbox/Notifications.tsx b/src/app/pages/client/inbox/Notifications.tsx
index 9592224f..4cc94d91 100644
--- a/src/app/pages/client/inbox/Notifications.tsx
+++ b/src/app/pages/client/inbox/Notifications.tsx
@@ -417,12 +417,7 @@ function RoomNotificationsGroupComp({
src={getRoomAvatarUrl(mx, room, 96, useAuthentication)}
alt={room.name}
renderFallback={() => (
-
+
)}
/>
diff --git a/src/app/pages/client/sidebar/CreateTab.tsx b/src/app/pages/client/sidebar/CreateTab.tsx
new file mode 100644
index 00000000..e6575cb4
--- /dev/null
+++ b/src/app/pages/client/sidebar/CreateTab.tsx
@@ -0,0 +1,134 @@
+import React, { MouseEventHandler, useState } from 'react';
+import { Box, config, Icon, Icons, Menu, PopOut, RectCords, Text } from 'folds';
+import FocusTrap from 'focus-trap-react';
+import { useNavigate } from 'react-router-dom';
+import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '../../../components/sidebar';
+import { stopPropagation } from '../../../utils/keyboard';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SettingTile } from '../../../components/setting-tile';
+import { ContainerColor } from '../../../styles/ContainerColor.css';
+import {
+ encodeSearchParamValueArray,
+ getCreatePath,
+ getSpacePath,
+ withSearchParam,
+} from '../../pathUtils';
+import { useCreateSelected } from '../../../hooks/router/useCreateSelected';
+import { JoinAddressPrompt } from '../../../components/join-address-prompt';
+import { _RoomSearchParams } from '../../paths';
+
+export function CreateTab() {
+ const createSelected = useCreateSelected();
+
+ const navigate = useNavigate();
+ const [menuCords, setMenuCords] = useState();
+ const [joinAddress, setJoinAddress] = useState(false);
+
+ const handleMenu: MouseEventHandler = (evt) => {
+ setMenuCords(menuCords ? undefined : evt.currentTarget.getBoundingClientRect());
+ };
+
+ const handleCreateSpace = () => {
+ navigate(getCreatePath());
+ setMenuCords(undefined);
+ };
+
+ const handleJoinWithAddress = () => {
+ setJoinAddress(true);
+ setMenuCords(undefined);
+ };
+
+ return (
+
+
+ {(triggerRef) => (
+ setMenuCords(undefined),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) =>
+ evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+ isKeyBackward: (evt: KeyboardEvent) =>
+ evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+
+
+
+ }>
+ Create Space
+
+ Build a space for your community.
+
+
+
+
+ }>
+ Join with Address
+
+ Become a part of existing community.
+
+
+
+
+
+
+ }
+ >
+
+
+
+ {joinAddress && (
+ setJoinAddress(false)}
+ onOpen={(roomIdOrAlias, viaServers) => {
+ setJoinAddress(false);
+ const path = getSpacePath(roomIdOrAlias);
+ navigate(
+ viaServers
+ ? withSearchParam<_RoomSearchParams>(path, {
+ viaServers: encodeSearchParamValueArray(viaServers),
+ })
+ : path
+ );
+ }}
+ />
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/src/app/pages/client/sidebar/DirectTab.tsx b/src/app/pages/client/sidebar/DirectTab.tsx
new file mode 100644
index 00000000..dcd44e2f
--- /dev/null
+++ b/src/app/pages/client/sidebar/DirectTab.tsx
@@ -0,0 +1,138 @@
+import React, { MouseEventHandler, forwardRef, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Box, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text, config, toRem } from 'folds';
+import FocusTrap from 'focus-trap-react';
+import { useAtomValue } from 'jotai';
+import { useDirects } from '../../../state/hooks/roomList';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { mDirectAtom } from '../../../state/mDirectList';
+import { allRoomsAtom } from '../../../state/room-list/roomList';
+import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
+import { getDirectPath, joinPathComponent } from '../../pathUtils';
+import { useRoomsUnread } from '../../../state/hooks/unread';
+import {
+ SidebarAvatar,
+ SidebarItem,
+ SidebarItemBadge,
+ SidebarItemTooltip,
+} from '../../../components/sidebar';
+import { useDirectSelected } from '../../../hooks/router/useDirectSelected';
+import { UnreadBadge } from '../../../components/unread-badge';
+import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
+import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
+import { useDirectRooms } from '../direct/useDirectRooms';
+import { markAsRead } from '../../../utils/notifications';
+import { stopPropagation } from '../../../utils/keyboard';
+import { settingsAtom } from '../../../state/settings';
+import { useSetting } from '../../../state/hooks/settings';
+
+type DirectMenuProps = {
+ requestClose: () => void;
+};
+const DirectMenu = forwardRef(({ requestClose }, ref) => {
+ const orphanRooms = useDirectRooms();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
+ const mx = useMatrixClient();
+
+ const handleMarkAsRead = () => {
+ if (!unread) return;
+ orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
+ requestClose();
+ };
+
+ return (
+
+
+ }
+ radii="300"
+ aria-disabled={!unread}
+ >
+
+ Mark as Read
+
+
+
+
+ );
+});
+
+export function DirectTab() {
+ const navigate = useNavigate();
+ const mx = useMatrixClient();
+ const screenSize = useScreenSizeContext();
+ const navToActivePath = useAtomValue(useNavToActivePathAtom());
+
+ const mDirects = useAtomValue(mDirectAtom);
+ const directs = useDirects(mx, allRoomsAtom, mDirects);
+ const directUnread = useRoomsUnread(directs, roomToUnreadAtom);
+ const [menuAnchor, setMenuAnchor] = useState();
+
+ const directSelected = useDirectSelected();
+
+ const handleDirectClick = () => {
+ const activePath = navToActivePath.get('direct');
+ if (activePath && screenSize !== ScreenSize.Mobile) {
+ navigate(joinPathComponent(activePath));
+ return;
+ }
+
+ navigate(getDirectPath());
+ };
+
+ const handleContextMenu: MouseEventHandler = (evt) => {
+ evt.preventDefault();
+ const cords = evt.currentTarget.getBoundingClientRect();
+ setMenuAnchor((currentState) => {
+ if (currentState) return undefined;
+ return cords;
+ });
+ };
+ return (
+
+
+ {(triggerRef) => (
+
+
+
+ )}
+
+ {directUnread && (
+ 0}>
+ 0} count={directUnread.total} />
+
+ )}
+ {menuAnchor && (
+ setMenuAnchor(undefined),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
+ isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+ setMenuAnchor(undefined)} />
+
+ }
+ />
+ )}
+
+ );
+}
diff --git a/src/app/pages/client/sidebar/ExploreTab.tsx b/src/app/pages/client/sidebar/ExploreTab.tsx
new file mode 100644
index 00000000..a45b5830
--- /dev/null
+++ b/src/app/pages/client/sidebar/ExploreTab.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { Icon, Icons } from 'folds';
+import { useNavigate } from 'react-router-dom';
+import { useAtomValue } from 'jotai';
+import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '../../../components/sidebar';
+import { useExploreSelected } from '../../../hooks/router/useExploreSelected';
+import {
+ getExploreFeaturedPath,
+ getExplorePath,
+ getExploreServerPath,
+ joinPathComponent,
+} from '../../pathUtils';
+import { useClientConfig } from '../../../hooks/useClientConfig';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { getMxIdServer } from '../../../utils/matrix';
+import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
+import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
+
+export function ExploreTab() {
+ const mx = useMatrixClient();
+ const screenSize = useScreenSizeContext();
+ const clientConfig = useClientConfig();
+ const navigate = useNavigate();
+ const navToActivePath = useAtomValue(useNavToActivePathAtom());
+
+ const exploreSelected = useExploreSelected();
+
+ const handleExploreClick = () => {
+ if (screenSize === ScreenSize.Mobile) {
+ navigate(getExplorePath());
+ return;
+ }
+
+ const activePath = navToActivePath.get('explore');
+ if (activePath) {
+ navigate(joinPathComponent(activePath));
+ return;
+ }
+
+ if (clientConfig.featuredCommunities?.openAsDefault) {
+ navigate(getExploreFeaturedPath());
+ return;
+ }
+ const userId = mx.getUserId();
+ const userServer = userId ? getMxIdServer(userId) : undefined;
+ if (userServer) {
+ navigate(getExploreServerPath(userServer));
+ return;
+ }
+ navigate(getExplorePath());
+ };
+
+ return (
+
+
+ {(triggerRef) => (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/app/pages/client/sidebar/HomeTab.tsx b/src/app/pages/client/sidebar/HomeTab.tsx
new file mode 100644
index 00000000..778b729e
--- /dev/null
+++ b/src/app/pages/client/sidebar/HomeTab.tsx
@@ -0,0 +1,140 @@
+import React, { MouseEventHandler, forwardRef, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Box, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text, config, toRem } from 'folds';
+import { useAtomValue } from 'jotai';
+import FocusTrap from 'focus-trap-react';
+import { useOrphanRooms } from '../../../state/hooks/roomList';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { mDirectAtom } from '../../../state/mDirectList';
+import { roomToParentsAtom } from '../../../state/room/roomToParents';
+import { allRoomsAtom } from '../../../state/room-list/roomList';
+import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
+import { getHomePath, joinPathComponent } from '../../pathUtils';
+import { useRoomsUnread } from '../../../state/hooks/unread';
+import {
+ SidebarAvatar,
+ SidebarItem,
+ SidebarItemBadge,
+ SidebarItemTooltip,
+} from '../../../components/sidebar';
+import { useHomeSelected } from '../../../hooks/router/useHomeSelected';
+import { UnreadBadge } from '../../../components/unread-badge';
+import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
+import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
+import { useHomeRooms } from '../home/useHomeRooms';
+import { markAsRead } from '../../../utils/notifications';
+import { stopPropagation } from '../../../utils/keyboard';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
+
+type HomeMenuProps = {
+ requestClose: () => void;
+};
+const HomeMenu = forwardRef(({ requestClose }, ref) => {
+ const orphanRooms = useHomeRooms();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
+ const mx = useMatrixClient();
+
+ const handleMarkAsRead = () => {
+ if (!unread) return;
+ orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
+ requestClose();
+ };
+
+ return (
+
+
+ }
+ radii="300"
+ aria-disabled={!unread}
+ >
+
+ Mark as Read
+
+
+
+
+ );
+});
+
+export function HomeTab() {
+ const navigate = useNavigate();
+ const mx = useMatrixClient();
+ const screenSize = useScreenSizeContext();
+ const navToActivePath = useAtomValue(useNavToActivePathAtom());
+
+ const mDirects = useAtomValue(mDirectAtom);
+ const roomToParents = useAtomValue(roomToParentsAtom);
+ const orphanRooms = useOrphanRooms(mx, allRoomsAtom, mDirects, roomToParents);
+ const homeUnread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
+ const homeSelected = useHomeSelected();
+ const [menuAnchor, setMenuAnchor] = useState();
+
+ const handleHomeClick = () => {
+ const activePath = navToActivePath.get('home');
+ if (activePath && screenSize !== ScreenSize.Mobile) {
+ navigate(joinPathComponent(activePath));
+ return;
+ }
+
+ navigate(getHomePath());
+ };
+
+ const handleContextMenu: MouseEventHandler = (evt) => {
+ evt.preventDefault();
+ const cords = evt.currentTarget.getBoundingClientRect();
+ setMenuAnchor((currentState) => {
+ if (currentState) return undefined;
+ return cords;
+ });
+ };
+
+ return (
+
+
+ {(triggerRef) => (
+
+
+
+ )}
+
+ {homeUnread && (
+ 0}>
+ 0} count={homeUnread.total} />
+
+ )}
+ {menuAnchor && (
+ setMenuAnchor(undefined),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
+ isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+ setMenuAnchor(undefined)} />
+
+ }
+ />
+ )}
+
+ );
+}
diff --git a/src/app/pages/client/sidebar/SearchTab.tsx b/src/app/pages/client/sidebar/SearchTab.tsx
new file mode 100644
index 00000000..7ceb5c49
--- /dev/null
+++ b/src/app/pages/client/sidebar/SearchTab.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { Icon, Icons } from 'folds';
+import { useAtom } from 'jotai';
+import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '../../../components/sidebar';
+import { searchModalAtom } from '../../../state/searchModal';
+
+export function SearchTab() {
+ const [opened, setOpen] = useAtom(searchModalAtom);
+
+ const open = () => setOpen(true);
+
+ return (
+
+
+ {(triggerRef) => (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx
new file mode 100644
index 00000000..6fc528a1
--- /dev/null
+++ b/src/app/pages/client/sidebar/SpaceTabs.tsx
@@ -0,0 +1,866 @@
+import React, {
+ MouseEventHandler,
+ ReactNode,
+ RefObject,
+ forwardRef,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import { useNavigate } from 'react-router-dom';
+import {
+ Box,
+ Icon,
+ IconButton,
+ Icons,
+ Line,
+ Menu,
+ MenuItem,
+ PopOut,
+ RectCords,
+ Text,
+ config,
+ toRem,
+} from 'folds';
+import { useAtom, useAtomValue } from 'jotai';
+import { Room } from 'matrix-js-sdk';
+import {
+ draggable,
+ dropTargetForElements,
+ monitorForElements,
+} from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
+import {
+ attachInstruction,
+ extractInstruction,
+ Instruction,
+} from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
+import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
+import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
+import FocusTrap from 'focus-trap-react';
+import {
+ useOrphanSpaces,
+ useRecursiveChildScopeFactory,
+ useSpaceChildren,
+} from '../../../state/hooks/roomList';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { roomToParentsAtom } from '../../../state/room/roomToParents';
+import { allRoomsAtom } from '../../../state/room-list/roomList';
+import { getSpaceLobbyPath, getSpacePath, joinPathComponent } from '../../pathUtils';
+import {
+ SidebarAvatar,
+ SidebarItem,
+ SidebarItemBadge,
+ SidebarItemTooltip,
+ SidebarStack,
+ SidebarStackSeparator,
+ SidebarFolder,
+ SidebarFolderDropTarget,
+} from '../../../components/sidebar';
+import { RoomUnreadProvider, RoomsUnreadProvider } from '../../../components/RoomUnreadProvider';
+import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace';
+import { UnreadBadge } from '../../../components/unread-badge';
+import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix';
+import { RoomAvatar } from '../../../components/room-avatar';
+import { nameInitials, randomStr } from '../../../utils/common';
+import {
+ ISidebarFolder,
+ SidebarItems,
+ TSidebarItem,
+ makeCinnySpacesContent,
+ parseSidebar,
+ sidebarItemWithout,
+ useSidebarItems,
+} from '../../../hooks/useSidebarItems';
+import { AccountDataEvent } from '../../../../types/matrix/accountData';
+import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
+import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
+import { useOpenedSidebarFolderAtom } from '../../../state/hooks/openedSidebarFolder';
+import { usePowerLevels } from '../../../hooks/usePowerLevels';
+import { useRoomsUnread } from '../../../state/hooks/unread';
+import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
+import { markAsRead } from '../../../utils/notifications';
+import { copyToClipboard } from '../../../utils/dom';
+import { stopPropagation } from '../../../utils/keyboard';
+import { getMatrixToRoom } from '../../../plugins/matrix-to';
+import { getViaServers } from '../../../plugins/via-servers';
+import { getRoomAvatarUrl } from '../../../utils/room';
+import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
+import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
+import { InviteUserPrompt } from '../../../components/invite-user-prompt';
+
+type SpaceMenuProps = {
+ room: Room;
+ requestClose: () => void;
+ onUnpin?: (roomId: string) => void;
+};
+const SpaceMenu = forwardRef(
+ ({ room, requestClose, onUnpin }, ref) => {
+ const mx = useMatrixClient();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const roomToParents = useAtomValue(roomToParentsAtom);
+ const powerLevels = usePowerLevels(room);
+ const creators = useRoomCreators(room);
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+ const canInvite = permissions.action('invite', mx.getSafeUserId());
+ const openSpaceSettings = useOpenSpaceSettings();
+
+ const [invitePrompt, setInvitePrompt] = useState(false);
+
+ const allChild = useSpaceChildren(
+ allRoomsAtom,
+ room.roomId,
+ useRecursiveChildScopeFactory(mx, roomToParents)
+ );
+ const unread = useRoomsUnread(allChild, roomToUnreadAtom);
+
+ const handleMarkAsRead = () => {
+ allChild.forEach((childRoomId) => markAsRead(mx, childRoomId, hideActivity));
+ requestClose();
+ };
+
+ const handleUnpin = () => {
+ onUnpin?.(room.roomId);
+ requestClose();
+ };
+
+ const handleCopyLink = () => {
+ const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
+ const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
+ copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
+ requestClose();
+ };
+
+ const handleInvite = () => {
+ setInvitePrompt(true);
+ };
+
+ const handleRoomSettings = () => {
+ openSpaceSettings(room.roomId);
+ requestClose();
+ };
+
+ return (
+
+ {invitePrompt && room && (
+ {
+ setInvitePrompt(false);
+ requestClose();
+ }}
+ />
+ )}
+
+ }
+ radii="300"
+ disabled={!unread}
+ >
+
+ Mark as Read
+
+
+ {onUnpin && (
+ }
+ >
+
+ Unpin
+
+
+ )}
+
+
+
+ }
+ radii="300"
+ aria-pressed={invitePrompt}
+ disabled={!canInvite}
+ >
+
+ Invite
+
+
+ }
+ radii="300"
+ >
+
+ Copy Link
+
+
+ }
+ radii="300"
+ >
+
+ Space Settings
+
+
+
+
+ );
+ }
+);
+
+type InstructionType = Instruction['type'];
+type FolderDraggable = {
+ folder: ISidebarFolder;
+ spaceId?: string;
+ open?: boolean;
+};
+type SidebarDraggable = string | FolderDraggable;
+
+const useDraggableItem = (
+ item: SidebarDraggable,
+ targetRef: RefObject,
+ onDragging: (item?: SidebarDraggable) => void,
+ dragHandleRef?: RefObject
+): boolean => {
+ const [dragging, setDragging] = useState(false);
+
+ useEffect(() => {
+ const target = targetRef.current;
+ const dragHandle = dragHandleRef?.current ?? undefined;
+
+ return !target
+ ? undefined
+ : draggable({
+ element: target,
+ dragHandle,
+ getInitialData: () => ({ item }),
+ onDragStart: () => {
+ setDragging(true);
+ onDragging?.(item);
+ },
+ onDrop: () => {
+ setDragging(false);
+ onDragging?.(undefined);
+ },
+ });
+ }, [targetRef, dragHandleRef, item, onDragging]);
+
+ return dragging;
+};
+
+const useDropTarget = (
+ item: SidebarDraggable,
+ targetRef: RefObject
+): Instruction | undefined => {
+ const [dropState, setDropState] = useState();
+
+ useEffect(() => {
+ const target = targetRef.current;
+ if (!target) return undefined;
+
+ return dropTargetForElements({
+ element: target,
+ canDrop: ({ source }) => {
+ const dragItem = source.data.item as SidebarDraggable;
+ return dragItem !== item;
+ },
+ getData: ({ input, element }) => {
+ const block: Instruction['type'][] = ['reparent'];
+ if (typeof item === 'object' && item.spaceId) block.push('make-child');
+
+ const insData = attachInstruction(
+ {},
+ {
+ input,
+ element,
+ currentLevel: 0,
+ indentPerLevel: 0,
+ mode: 'standard',
+ block,
+ }
+ );
+
+ const instruction: Instruction | null = extractInstruction(insData);
+ setDropState(instruction ?? undefined);
+
+ return {
+ item,
+ instructionType: instruction ? instruction.type : undefined,
+ };
+ },
+ onDragLeave: () => setDropState(undefined),
+ onDrop: () => setDropState(undefined),
+ });
+ }, [item, targetRef]);
+
+ return dropState;
+};
+
+function useDropTargetInstruction(
+ item: SidebarDraggable,
+ targetRef: RefObject,
+ instructionType: T
+): T | undefined {
+ const [dropState, setDropState] = useState();
+
+ useEffect(() => {
+ const target = targetRef.current;
+ if (!target) return undefined;
+
+ return dropTargetForElements({
+ element: target,
+ canDrop: ({ source }) => {
+ const dragItem = source.data.item as SidebarDraggable;
+ return dragItem !== item;
+ },
+ getData: () => {
+ setDropState(instructionType);
+
+ return {
+ item,
+ instructionType,
+ };
+ },
+ onDragLeave: () => setDropState(undefined),
+ onDrop: () => setDropState(undefined),
+ });
+ }, [item, targetRef, instructionType]);
+
+ return dropState;
+}
+
+const useDnDMonitor = (
+ scrollRef: RefObject,
+ onDragging: (dragItem?: SidebarDraggable) => void,
+ onReorder: (
+ draggable: SidebarDraggable,
+ container: SidebarDraggable,
+ instruction: InstructionType
+ ) => void
+) => {
+ useEffect(() => {
+ const scrollElement = scrollRef.current;
+ if (!scrollElement) {
+ throw Error('Scroll element ref not configured');
+ }
+
+ return combine(
+ monitorForElements({
+ onDrop: ({ source, location }) => {
+ onDragging(undefined);
+ const { dropTargets } = location.current;
+ if (dropTargets.length === 0) return;
+ const item = source.data.item as SidebarDraggable;
+ const containerItem = dropTargets[0].data.item as SidebarDraggable;
+ const instructionType = dropTargets[0].data.instructionType as
+ | InstructionType
+ | undefined;
+ if (!instructionType) return;
+ onReorder(item, containerItem, instructionType);
+ },
+ }),
+ autoScrollForElements({
+ element: scrollElement,
+ })
+ );
+ }, [scrollRef, onDragging, onReorder]);
+};
+
+type SpaceTabProps = {
+ space: Room;
+ selected: boolean;
+ onClick: MouseEventHandler;
+ folder?: ISidebarFolder;
+ onDragging: (dragItem?: SidebarDraggable) => void;
+ disabled?: boolean;
+ onUnpin?: (roomId: string) => void;
+};
+function SpaceTab({
+ space,
+ selected,
+ onClick,
+ folder,
+ onDragging,
+ disabled,
+ onUnpin,
+}: SpaceTabProps) {
+ const mx = useMatrixClient();
+ const useAuthentication = useMediaAuthentication();
+ const targetRef = useRef(null);
+
+ const spaceDraggable: SidebarDraggable = useMemo(
+ () =>
+ folder
+ ? {
+ folder,
+ spaceId: space.roomId,
+ }
+ : space.roomId,
+ [folder, space]
+ );
+
+ useDraggableItem(spaceDraggable, targetRef, onDragging);
+ const dropState = useDropTarget(spaceDraggable, targetRef);
+ const dropType = dropState?.type;
+
+ const [menuAnchor, setMenuAnchor] = useState();
+
+ const handleContextMenu: MouseEventHandler = (evt) => {
+ evt.preventDefault();
+ const cords = evt.currentTarget.getBoundingClientRect();
+ setMenuAnchor((currentState) => {
+ if (currentState) return undefined;
+ return cords;
+ });
+ };
+
+ return (
+
+ {(unread) => (
+
+
+ {(triggerRef) => (
+
+ (
+ {nameInitials(space.name, 2)}
+ )}
+ />
+
+ )}
+
+ {unread && (
+ 0}>
+ 0} count={unread.total} />
+
+ )}
+ {menuAnchor && (
+ setMenuAnchor(undefined),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
+ isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+ setMenuAnchor(undefined)}
+ onUnpin={onUnpin}
+ />
+
+ }
+ />
+ )}
+
+ )}
+
+ );
+}
+
+type OpenedSpaceFolderProps = {
+ folder: ISidebarFolder;
+ onClose: MouseEventHandler;
+ children?: ReactNode;
+};
+function OpenedSpaceFolder({ folder, onClose, children }: OpenedSpaceFolderProps) {
+ const aboveTargetRef = useRef(null);
+ const belowTargetRef = useRef(null);
+
+ const spaceDraggable: SidebarDraggable = useMemo(() => ({ folder, open: true }), [folder]);
+
+ const orderAbove = useDropTargetInstruction(spaceDraggable, aboveTargetRef, 'reorder-above');
+ const orderBelow = useDropTargetInstruction(spaceDraggable, belowTargetRef, 'reorder-below');
+
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+ );
+}
+
+type ClosedSpaceFolderProps = {
+ folder: ISidebarFolder;
+ selected: boolean;
+ onOpen: MouseEventHandler;
+ onDragging: (dragItem?: SidebarDraggable) => void;
+ disabled?: boolean;
+};
+function ClosedSpaceFolder({
+ folder,
+ selected,
+ onOpen,
+ onDragging,
+ disabled,
+}: ClosedSpaceFolderProps) {
+ const mx = useMatrixClient();
+ const useAuthentication = useMediaAuthentication();
+ const handlerRef = useRef(null);
+
+ const spaceDraggable: FolderDraggable = useMemo(() => ({ folder }), [folder]);
+ useDraggableItem(spaceDraggable, handlerRef, onDragging);
+ const dropState = useDropTarget(spaceDraggable, handlerRef);
+ const dropType = dropState?.type;
+
+ const tooltipName =
+ folder.name ?? folder.content.map((i) => mx.getRoom(i)?.name ?? '').join(', ') ?? 'Unnamed';
+
+ return (
+
+ {(unread) => (
+
+
+ {(tooltipRef) => (
+
+ {folder.content.map((sId) => {
+ const space = mx.getRoom(sId);
+ if (!space) return null;
+
+ return (
+
+ (
+
+ {nameInitials(space.name, 2)}
+
+ )}
+ />
+
+ );
+ })}
+
+ )}
+
+ {unread && (
+ 0}>
+ 0} count={unread.total} />
+
+ )}
+
+ )}
+
+ );
+}
+
+type SpaceTabsProps = {
+ scrollRef: RefObject;
+};
+export function SpaceTabs({ scrollRef }: SpaceTabsProps) {
+ const navigate = useNavigate();
+ const mx = useMatrixClient();
+ const screenSize = useScreenSizeContext();
+ const roomToParents = useAtomValue(roomToParentsAtom);
+ const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, roomToParents);
+ const [sidebarItems, localEchoSidebarItem] = useSidebarItems(orphanSpaces);
+ const navToActivePath = useAtomValue(useNavToActivePathAtom());
+ const [openedFolder, setOpenedFolder] = useAtom(useOpenedSidebarFolderAtom());
+ const [draggingItem, setDraggingItem] = useState();
+
+ useDnDMonitor(
+ scrollRef,
+ setDraggingItem,
+ useCallback(
+ (item, containerItem, instructionType) => {
+ const newItems: SidebarItems = [];
+
+ const matchDest = (sI: TSidebarItem, dI: SidebarDraggable): boolean => {
+ if (typeof sI === 'string' && typeof dI === 'string') {
+ return sI === dI;
+ }
+ if (typeof sI === 'object' && typeof dI === 'object') {
+ return sI.id === dI.folder.id;
+ }
+ return false;
+ };
+ const itemAsFolderContent = (i: SidebarDraggable): string[] => {
+ if (typeof i === 'string') {
+ return [i];
+ }
+ if (i.spaceId) {
+ return [i.spaceId];
+ }
+ return [...i.folder.content];
+ };
+
+ sidebarItems.forEach((i) => {
+ const sameFolders =
+ typeof item === 'object' &&
+ typeof containerItem === 'object' &&
+ item.folder.id === containerItem.folder.id;
+
+ // remove draggable space from current position or folder
+ if (!sameFolders && matchDest(i, item)) {
+ if (typeof item === 'object' && item.spaceId) {
+ const folderContent = item.folder.content.filter((s) => s !== item.spaceId);
+ if (folderContent.length === 0) {
+ // remove open state from local storage
+ setOpenedFolder({ type: 'DELETE', id: item.folder.id });
+ return;
+ }
+ newItems.push({
+ ...item.folder,
+ content: folderContent,
+ });
+ }
+ return;
+ }
+ if (matchDest(i, containerItem)) {
+ // we can make child only if
+ // container item is space or closed folder
+ if (instructionType === 'make-child') {
+ const child: string[] = itemAsFolderContent(item);
+ if (typeof containerItem === 'string') {
+ const folder: ISidebarFolder = {
+ id: randomStr(),
+ content: [containerItem].concat(child),
+ };
+ newItems.push(folder);
+ return;
+ }
+ newItems.push({
+ ...containerItem.folder,
+ content: containerItem.folder.content.concat(child),
+ });
+ return;
+ }
+
+ // drop inside opened folder
+ // or reordering inside same folder
+ if (typeof containerItem === 'object' && containerItem.spaceId) {
+ const child = itemAsFolderContent(item);
+ const newContent: string[] = [];
+ containerItem.folder.content
+ .filter((sId) => !child.includes(sId))
+ .forEach((sId) => {
+ if (sId === containerItem.spaceId) {
+ if (instructionType === 'reorder-below') {
+ newContent.push(sId, ...child);
+ }
+ if (instructionType === 'reorder-above') {
+ newContent.push(...child, sId);
+ }
+ return;
+ }
+ newContent.push(sId);
+ });
+ const folder = {
+ ...containerItem.folder,
+ content: newContent,
+ };
+
+ newItems.push(folder);
+ return;
+ }
+
+ // drop above or below space or closed/opened folder
+ if (typeof item === 'string') {
+ if (instructionType === 'reorder-below') newItems.push(i);
+ newItems.push(item);
+ if (instructionType === 'reorder-above') newItems.push(i);
+ } else if (item.spaceId) {
+ if (instructionType === 'reorder-above') {
+ newItems.push(item.spaceId);
+ }
+ if (sameFolders && typeof i === 'object') {
+ // remove from folder if placing around itself
+ const newI = { ...i, content: i.content.filter((sId) => sId !== item.spaceId) };
+ if (newI.content.length > 0) newItems.push(newI);
+ } else {
+ newItems.push(i);
+ }
+ if (instructionType === 'reorder-below') {
+ newItems.push(item.spaceId);
+ }
+ } else {
+ if (instructionType === 'reorder-below') newItems.push(i);
+ newItems.push(item.folder);
+ if (instructionType === 'reorder-above') newItems.push(i);
+ }
+ return;
+ }
+ newItems.push(i);
+ });
+
+ const newSpacesContent = makeCinnySpacesContent(mx, newItems);
+ localEchoSidebarItem(parseSidebar(mx, orphanSpaces, newSpacesContent));
+ mx.setAccountData(AccountDataEvent.CinnySpaces, newSpacesContent);
+ },
+ [mx, sidebarItems, setOpenedFolder, localEchoSidebarItem, orphanSpaces]
+ )
+ );
+
+ const selectedSpaceId = useSelectedSpace();
+
+ const handleSpaceClick: MouseEventHandler = (evt) => {
+ const target = evt.currentTarget;
+ const targetSpaceId = target.getAttribute('data-id');
+ if (!targetSpaceId) return;
+
+ const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, targetSpaceId));
+ if (screenSize === ScreenSize.Mobile) {
+ navigate(spacePath);
+ return;
+ }
+
+ const activePath = navToActivePath.get(targetSpaceId);
+ if (activePath && activePath.pathname.startsWith(spacePath)) {
+ navigate(joinPathComponent(activePath));
+ return;
+ }
+
+ navigate(getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, targetSpaceId)));
+ };
+
+ const handleFolderToggle: MouseEventHandler = (evt) => {
+ const target = evt.currentTarget;
+ const targetFolderId = target.getAttribute('data-id');
+ if (!targetFolderId) return;
+
+ setOpenedFolder({
+ type: openedFolder.has(targetFolderId) ? 'DELETE' : 'PUT',
+ id: targetFolderId,
+ });
+ };
+
+ const handleUnpin = useCallback(
+ (roomId: string) => {
+ if (orphanSpaces.includes(roomId)) return;
+ const newItems = sidebarItemWithout(sidebarItems, roomId);
+
+ const newSpacesContent = makeCinnySpacesContent(mx, newItems);
+ localEchoSidebarItem(parseSidebar(mx, orphanSpaces, newSpacesContent));
+ mx.setAccountData(AccountDataEvent.CinnySpaces, newSpacesContent);
+ },
+ [mx, sidebarItems, orphanSpaces, localEchoSidebarItem]
+ );
+
+ if (sidebarItems.length === 0) return null;
+ return (
+ <>
+
+
+ {sidebarItems.map((item) => {
+ if (typeof item === 'object') {
+ if (openedFolder.has(item.id)) {
+ return (
+
+ {item.content.map((sId) => {
+ const space = mx.getRoom(sId);
+ if (!space) return null;
+ return (
+
+ );
+ })}
+
+ );
+ }
+
+ return (
+
+ );
+ }
+
+ const space = mx.getRoom(item);
+ if (!space) return null;
+
+ return (
+
+ );
+ })}
+
+ >
+ );
+}
diff --git a/src/app/pages/client/sidebar/index.ts b/src/app/pages/client/sidebar/index.ts
index 3cf570d9..d44cfaa2 100644
--- a/src/app/pages/client/sidebar/index.ts
+++ b/src/app/pages/client/sidebar/index.ts
@@ -1,3 +1,8 @@
+export * from './HomeTab';
+export * from './DirectTab';
+export * from './SpaceTabs';
export * from './InboxTab';
+export * from './ExploreTab';
export * from './SettingsTab';
export * from './UnverifiedTab';
+export * from './SearchTab';
diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx
new file mode 100644
index 00000000..3f60d2a9
--- /dev/null
+++ b/src/app/pages/client/space/Space.tsx
@@ -0,0 +1,539 @@
+import React, {
+ MouseEventHandler,
+ forwardRef,
+ useCallback,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import { useAtom, useAtomValue } from 'jotai';
+import {
+ Avatar,
+ Box,
+ Button,
+ Icon,
+ IconButton,
+ Icons,
+ Line,
+ Menu,
+ MenuItem,
+ PopOut,
+ RectCords,
+ Spinner,
+ Text,
+ color,
+ config,
+ toRem,
+} from 'folds';
+import { useVirtualizer } from '@tanstack/react-virtual';
+import { JoinRule, Room } from 'matrix-js-sdk';
+import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
+import FocusTrap from 'focus-trap-react';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { mDirectAtom } from '../../../state/mDirectList';
+import {
+ NavCategory,
+ NavCategoryHeader,
+ NavItem,
+ NavItemContent,
+ NavLink,
+} from '../../../components/nav';
+import { getSpaceLobbyPath, getSpaceRoomPath, getSpaceSearchPath } from '../../pathUtils';
+import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix';
+import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
+import {
+ useSpaceLobbySelected,
+ useSpaceSearchSelected,
+} from '../../../hooks/router/useSelectedSpace';
+import { useSpace } from '../../../hooks/useSpace';
+import { VirtualTile } from '../../../components/virtualizer';
+import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
+import { makeNavCategoryId } from '../../../state/closedNavCategories';
+import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
+import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
+import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
+import { useRoomName } from '../../../hooks/useRoomMeta';
+import { useSpaceJoinedHierarchy } from '../../../hooks/useSpaceHierarchy';
+import { allRoomsAtom } from '../../../state/room-list/roomList';
+import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
+import { usePowerLevels } from '../../../hooks/usePowerLevels';
+import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList';
+import { roomToParentsAtom } from '../../../state/room/roomToParents';
+import { markAsRead } from '../../../utils/notifications';
+import { useRoomsUnread } from '../../../state/hooks/unread';
+import { UseStateProvider } from '../../../components/UseStateProvider';
+import { LeaveSpacePrompt } from '../../../components/leave-space-prompt';
+import { copyToClipboard } from '../../../utils/dom';
+import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
+import { useStateEvent } from '../../../hooks/useStateEvent';
+import { Membership, StateEvent } from '../../../../types/matrix/room';
+import { stopPropagation } from '../../../utils/keyboard';
+import { getMatrixToRoom } from '../../../plugins/matrix-to';
+import { getViaServers } from '../../../plugins/via-servers';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
+import {
+ getRoomNotificationMode,
+ useRoomsNotificationPreferencesContext,
+} from '../../../hooks/useRoomsNotificationPreferences';
+import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
+import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
+import { ContainerColor } from '../../../styles/ContainerColor.css';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { BreakWord } from '../../../styles/Text.css';
+import { InviteUserPrompt } from '../../../components/invite-user-prompt';
+
+type SpaceMenuProps = {
+ room: Room;
+ requestClose: () => void;
+};
+const SpaceMenu = forwardRef(({ room, requestClose }, ref) => {
+ const mx = useMatrixClient();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const [developerTools] = useSetting(settingsAtom, 'developerTools');
+ const roomToParents = useAtomValue(roomToParentsAtom);
+ const powerLevels = usePowerLevels(room);
+ const creators = useRoomCreators(room);
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+ const canInvite = permissions.action('invite', mx.getSafeUserId());
+ const openSpaceSettings = useOpenSpaceSettings();
+ const { navigateRoom } = useRoomNavigate();
+
+ const [invitePrompt, setInvitePrompt] = useState(false);
+
+ const allChild = useSpaceChildren(
+ allRoomsAtom,
+ room.roomId,
+ useRecursiveChildScopeFactory(mx, roomToParents)
+ );
+ const unread = useRoomsUnread(allChild, roomToUnreadAtom);
+
+ const handleMarkAsRead = () => {
+ allChild.forEach((childRoomId) => markAsRead(mx, childRoomId, hideActivity));
+ requestClose();
+ };
+
+ const handleCopyLink = () => {
+ const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
+ const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
+ copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
+ requestClose();
+ };
+
+ const handleInvite = () => {
+ setInvitePrompt(true);
+ };
+
+ const handleRoomSettings = () => {
+ openSpaceSettings(room.roomId);
+ requestClose();
+ };
+
+ const handleOpenTimeline = () => {
+ navigateRoom(room.roomId);
+ requestClose();
+ };
+
+ return (
+
+
+ {invitePrompt && room && (
+ {
+ setInvitePrompt(false);
+ requestClose();
+ }}
+ />
+ )}
+ }
+ radii="300"
+ disabled={!unread}
+ >
+
+ Mark as Read
+
+
+
+
+
+ }
+ radii="300"
+ aria-pressed={invitePrompt}
+ disabled={!canInvite}
+ >
+
+ Invite
+
+
+ }
+ radii="300"
+ >
+
+ Copy Link
+
+
+ }
+ radii="300"
+ >
+
+ Space Settings
+
+
+ {developerTools && (
+ }
+ radii="300"
+ >
+
+ Event Timeline
+
+
+ )}
+
+
+
+
+ {(promptLeave, setPromptLeave) => (
+ <>
+ setPromptLeave(true)}
+ variant="Critical"
+ fill="None"
+ size="300"
+ after={ }
+ radii="300"
+ aria-pressed={promptLeave}
+ >
+
+ Leave Space
+
+
+ {promptLeave && (
+ setPromptLeave(false)}
+ />
+ )}
+ >
+ )}
+
+
+
+ );
+});
+
+function SpaceHeader() {
+ const space = useSpace();
+ const spaceName = useRoomName(space);
+ const [menuAnchor, setMenuAnchor] = useState();
+
+ const joinRules = useStateEvent(
+ space,
+ StateEvent.RoomJoinRules
+ )?.getContent();
+
+ const handleOpenMenu: MouseEventHandler = (evt) => {
+ const cords = evt.currentTarget.getBoundingClientRect();
+ setMenuAnchor((currentState) => {
+ if (currentState) return undefined;
+ return cords;
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+ {spaceName}
+
+ {joinRules?.join_rule !== JoinRule.Public && }
+
+
+
+
+
+
+
+
+ {menuAnchor && (
+ setMenuAnchor(undefined),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
+ isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+ setMenuAnchor(undefined)} />
+
+ }
+ />
+ )}
+ >
+ );
+}
+
+type SpaceTombstoneProps = { roomId: string; replacementRoomId: string };
+export function SpaceTombstone({ roomId, replacementRoomId }: SpaceTombstoneProps) {
+ const mx = useMatrixClient();
+ const { navigateSpace } = useRoomNavigate();
+
+ const [joinState, handleJoin] = useAsyncCallback(
+ useCallback(() => {
+ const currentRoom = mx.getRoom(roomId);
+ const via = currentRoom ? getViaServers(currentRoom) : [];
+ return mx.joinRoom(replacementRoomId, {
+ viaServers: via,
+ });
+ }, [mx, roomId, replacementRoomId])
+ );
+ const replacementRoom = mx.getRoom(replacementRoomId);
+
+ const handleOpen = () => {
+ if (replacementRoom) navigateSpace(replacementRoom.roomId);
+ if (joinState.status === AsyncStatus.Success) navigateSpace(joinState.data.roomId);
+ };
+
+ return (
+
+
+ Space Upgraded
+ This space has been replaced and is no longer active.
+ {joinState.status === AsyncStatus.Error && (
+
+ {(joinState.error as any)?.message ?? 'Failed to join replacement space!'}
+
+ )}
+
+
+ {replacementRoom?.getMyMembership() === Membership.Join ||
+ joinState.status === AsyncStatus.Success ? (
+
+ Open New Space
+
+ ) : (
+
+ )
+ }
+ disabled={joinState.status === AsyncStatus.Loading}
+ >
+ Join New Space
+
+ )}
+
+
+ );
+}
+
+export function Space() {
+ const mx = useMatrixClient();
+ const space = useSpace();
+ useNavToActivePathMapper(space.roomId);
+ const spaceIdOrAlias = getCanonicalAliasOrRoomId(mx, space.roomId);
+ const scrollRef = useRef(null);
+ const mDirects = useAtomValue(mDirectAtom);
+ const roomToUnread = useAtomValue(roomToUnreadAtom);
+ const allRooms = useAtomValue(allRoomsAtom);
+ const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
+ const notificationPreferences = useRoomsNotificationPreferencesContext();
+
+ const tombstoneEvent = useStateEvent(space, StateEvent.RoomTombstone);
+
+ const selectedRoomId = useSelectedRoom();
+ const lobbySelected = useSpaceLobbySelected(spaceIdOrAlias);
+ const searchSelected = useSpaceSearchSelected(spaceIdOrAlias);
+
+ const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom());
+
+ const getRoom = useCallback(
+ (rId: string) => {
+ if (allJoinedRooms.has(rId)) {
+ return mx.getRoom(rId) ?? undefined;
+ }
+ return undefined;
+ },
+ [mx, allJoinedRooms]
+ );
+
+ const hierarchy = useSpaceJoinedHierarchy(
+ space.roomId,
+ getRoom,
+ useCallback(
+ (parentId, roomId) => {
+ if (!closedCategories.has(makeNavCategoryId(space.roomId, parentId))) {
+ return false;
+ }
+ const showRoom = roomToUnread.has(roomId) || roomId === selectedRoomId;
+ if (showRoom) return false;
+ return true;
+ },
+ [space.roomId, closedCategories, roomToUnread, selectedRoomId]
+ ),
+ useCallback(
+ (sId) => closedCategories.has(makeNavCategoryId(space.roomId, sId)),
+ [closedCategories, space.roomId]
+ )
+ );
+
+ const virtualizer = useVirtualizer({
+ count: hierarchy.length,
+ getScrollElement: () => scrollRef.current,
+ estimateSize: () => 0,
+ overscan: 10,
+ });
+
+ const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) =>
+ closedCategories.has(categoryId)
+ );
+
+ const getToLink = (roomId: string) =>
+ getSpaceRoomPath(spaceIdOrAlias, getCanonicalAliasOrRoomId(mx, roomId));
+
+ return (
+
+
+
+
+ {tombstoneEvent && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ Lobby
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Message Search
+
+
+
+
+
+
+
+
+ {virtualizer.getVirtualItems().map((vItem) => {
+ const { roomId } = hierarchy[vItem.index] ?? {};
+ const room = mx.getRoom(roomId);
+ if (!room) return null;
+
+ if (room.isSpaceRoom()) {
+ const categoryId = makeNavCategoryId(space.roomId, roomId);
+
+ return (
+
+
+
+
+ {roomId === space.roomId ? 'Rooms' : room?.name}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/src/app/pages/client/space/index.ts b/src/app/pages/client/space/index.ts
index 477d60ff..0e32d93b 100644
--- a/src/app/pages/client/space/index.ts
+++ b/src/app/pages/client/space/index.ts
@@ -1,3 +1,4 @@
export * from './SpaceProvider';
+export * from './Space';
export * from './Search';
export * from './RoomProvider';
diff --git a/src/app/plugins/call/CallControl.ts b/src/app/plugins/call/CallControl.ts
deleted file mode 100644
index 62f6966a..00000000
--- a/src/app/plugins/call/CallControl.ts
+++ /dev/null
@@ -1,289 +0,0 @@
-import { ClientWidgetApi } from 'matrix-widget-api';
-import EventEmitter from 'events';
-import { CallControlState } from './CallControlState';
-import { ElementMediaStateDetail, ElementMediaStatePayload, ElementWidgetActions } from './types';
-
-export enum CallControlEvent {
- StateUpdate = 'state_update',
-}
-
-export class CallControl extends EventEmitter implements CallControlState {
- private state: CallControlState;
-
- private call: ClientWidgetApi;
-
- private iframe: HTMLIFrameElement;
-
- private bodyMutationObserver: MutationObserver;
-
- private controlMutationObserver: MutationObserver;
-
- private mediaStatePromiseResolver: undefined | (() => void);
-
- private get document(): Document | undefined {
- return this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
- }
-
- private get screenshareButton(): HTMLElement | undefined {
- const screenshareBtn = this.document?.querySelector(
- '[data-testid="incall_screenshare"]'
- ) as HTMLElement | null;
-
- return screenshareBtn ?? undefined;
- }
-
- private get leaveButton(): Element | undefined {
- const leaveBtn = this.document?.querySelector('[data-testid="incall_leave"]');
-
- return leaveBtn ?? undefined;
- }
-
- private get settingsButton(): HTMLElement | undefined {
- const settingsButtonLeft = this.document?.querySelector(
- '[data-testid="settings-bottom-left"]'
- ) as HTMLButtonElement | undefined;
- const settingsButtonCenter = this.document?.querySelector(
- '[data-testid="settings-bottom-center"]'
- ) as HTMLButtonElement | undefined;
-
- return settingsButtonLeft ?? settingsButtonCenter ?? undefined;
- }
-
- private get reactionsButton(): HTMLElement | undefined {
- const reactionsButton = this.leaveButton?.previousElementSibling as HTMLElement | null;
-
- return reactionsButton ?? undefined;
- }
-
- private get spotlightButton(): HTMLInputElement | undefined {
- const spotlightButton = this.document?.querySelector(
- 'input[value="spotlight"]'
- ) as HTMLInputElement | null;
-
- return spotlightButton ?? undefined;
- }
-
- private get gridButton(): HTMLInputElement | undefined {
- const gridButton = this.document?.querySelector(
- 'input[value="grid"]'
- ) as HTMLInputElement | null;
-
- return gridButton ?? undefined;
- }
-
- constructor(state: CallControlState, call: ClientWidgetApi, iframe: HTMLIFrameElement) {
- super();
-
- this.state = state;
- this.call = call;
- this.iframe = iframe;
-
- this.bodyMutationObserver = new MutationObserver(this.onBodyMutation.bind(this));
- this.controlMutationObserver = new MutationObserver(this.onControlMutation.bind(this));
- }
-
- public getState(): CallControlState {
- return this.state;
- }
-
- public get microphone(): boolean {
- return this.state.microphone;
- }
-
- public get video(): boolean {
- return this.state.video;
- }
-
- public get sound(): boolean {
- return this.state.sound;
- }
-
- public get screenshare(): boolean {
- return this.state.screenshare;
- }
-
- public get spotlight(): boolean {
- return this.state.spotlight;
- }
-
- public async applyState() {
- await this.setMediaState({
- audio_enabled: this.microphone,
- video_enabled: this.video,
- });
- this.setSound(this.sound);
- this.emitStateUpdate();
- }
-
- public startObserving() {
- if (!this.document) return;
-
- this.bodyMutationObserver.observe(this.document.body, {
- childList: true,
- subtree: false, // only direct children of body
- });
- this.onBodyMutation();
- }
-
- private onBodyMutation() {
- if (!this.document) return;
-
- this.document.body.style.setProperty('background', 'none', 'important');
-
- const controls = this.leaveButton?.parentElement?.parentElement;
- if (controls) {
- controls.style.setProperty('position', 'absolute');
- controls.style.setProperty('visibility', 'hidden');
- }
-
- this.observeControls();
- }
-
- private observeControls() {
- this.controlMutationObserver.disconnect();
-
- const screenshareBtn = this.screenshareButton;
- if (screenshareBtn) {
- this.controlMutationObserver.observe(screenshareBtn, {
- attributes: true,
- attributeFilter: ['data-kind'],
- });
- }
- const spotlightBtn = this.spotlightButton;
- if (spotlightBtn) {
- this.controlMutationObserver.observe(spotlightBtn, {
- attributes: true,
- });
- }
-
- this.onControlMutation();
- }
-
- public applySound() {
- this.setSound(this.sound);
- }
-
- private async setMediaState(state: ElementMediaStatePayload) {
- const data = await this.call.transport.send(ElementWidgetActions.DeviceMute, state);
- return new Promise(resolve => {
- if (this.mediaStatePromiseResolver) {
- this.mediaStatePromiseResolver();
- }
- this.mediaStatePromiseResolver = () => resolve(data);
- });
- }
-
- private setSound(sound: boolean): void {
- const callDocument = this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
- if (callDocument) {
- callDocument.querySelectorAll('audio').forEach((el) => {
- // eslint-disable-next-line no-param-reassign
- el.muted = !sound;
- });
- }
- }
-
- public onMediaState(evt: CustomEvent) {
- const { data } = evt.detail;
- if (!data) return;
-
- const state = new CallControlState(
- data.audio_enabled ?? this.microphone,
- data.video_enabled ?? this.video,
- this.sound,
- this.screenshare,
- this.spotlight
- );
-
- this.state = state;
- this.emitStateUpdate();
-
- if (this.microphone && !this.sound) {
- this.toggleSound();
- }
-
- if (this.mediaStatePromiseResolver) {
- this.mediaStatePromiseResolver();
- this.mediaStatePromiseResolver = undefined;
- }
- }
-
- private onControlMutation() {
- const screenshare: boolean = this.screenshareButton?.getAttribute('data-kind') === 'primary';
- const spotlight: boolean = this.spotlightButton?.checked ?? false;
-
- this.state = new CallControlState(
- this.microphone,
- this.video,
- this.sound,
- screenshare,
- spotlight
- );
- this.emitStateUpdate();
- }
-
- public toggleMicrophone() {
- const payload: ElementMediaStatePayload = {
- audio_enabled: !this.microphone,
- video_enabled: this.video,
- };
- return this.setMediaState(payload);
- }
-
- public toggleVideo() {
- const payload: ElementMediaStatePayload = {
- audio_enabled: this.microphone,
- video_enabled: !this.video,
- };
- return this.setMediaState(payload);
- }
-
- public toggleSound() {
- const sound = !this.sound;
-
- this.setSound(sound);
-
- const state = new CallControlState(
- this.microphone,
- this.video,
- sound,
- this.screenshare,
- this.spotlight
- );
- this.state = state;
- this.emitStateUpdate();
-
- if (!this.sound && this.microphone) {
- this.toggleMicrophone();
- }
- }
-
- public toggleScreenshare() {
- this.screenshareButton?.click();
- }
-
- public toggleSpotlight() {
- if (this.spotlight) {
- this.gridButton?.click();
- return;
- }
- this.spotlightButton?.click();
- }
-
- public toggleReactions() {
- this.reactionsButton?.click();
- }
-
- public toggleSettings() {
- this.settingsButton?.click();
- }
-
- public dispose() {
- this.bodyMutationObserver.disconnect();
- this.controlMutationObserver.disconnect();
- }
-
- private emitStateUpdate() {
- this.emit(CallControlEvent.StateUpdate);
- }
-}
diff --git a/src/app/plugins/call/CallControlState.ts b/src/app/plugins/call/CallControlState.ts
deleted file mode 100644
index fc7e3f22..00000000
--- a/src/app/plugins/call/CallControlState.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-export class CallControlState {
- public readonly microphone: boolean;
-
- public readonly video: boolean;
-
- public readonly sound: boolean;
-
- public readonly screenshare: boolean;
-
- public readonly spotlight: boolean;
-
- constructor(
- microphone: boolean,
- video: boolean,
- sound: boolean,
- screenshare = false,
- spotlight = false
- ) {
- this.microphone = microphone;
- this.video = video;
- this.sound = sound;
- this.screenshare = screenshare;
- this.spotlight = spotlight;
- }
-}
diff --git a/src/app/plugins/call/CallEmbed.ts b/src/app/plugins/call/CallEmbed.ts
deleted file mode 100644
index 8d03bc8a..00000000
--- a/src/app/plugins/call/CallEmbed.ts
+++ /dev/null
@@ -1,416 +0,0 @@
-import {
- ClientEvent,
- KnownMembership,
- MatrixClient,
- MatrixEvent,
- MatrixEventEvent,
- Room,
- RoomStateEvent,
-} from 'matrix-js-sdk';
-import {
- ClientWidgetApi,
- IRoomEvent,
- IWidget,
- Widget,
- WidgetApiToWidgetAction,
- WidgetDriver,
-} from 'matrix-widget-api';
-import { CallWidgetDriver } from './CallWidgetDriver';
-import { trimTrailingSlash } from '../../utils/common';
-import {
- ElementCallIntent,
- ElementCallThemeKind,
- ElementMediaStateDetail,
- ElementWidgetActions,
-} from './types';
-import { CallControl } from './CallControl';
-import { CallControlState } from './CallControlState';
-
-export class CallEmbed {
- private mx: MatrixClient;
-
- public readonly call: ClientWidgetApi;
-
- public readonly iframe: HTMLIFrameElement;
-
- public readonly room: Room;
-
- public joined = false;
-
- public readonly control: CallControl;
-
- private readonly container: HTMLElement;
-
- private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID
-
- private eventsToFeed = new WeakSet();
-
- private readonly disposables: Array<() => void> = [];
-
- static getIntent(dm: boolean, ongoing: boolean, video?: boolean): ElementCallIntent {
- if (dm && ongoing) {
- return video ? ElementCallIntent.JoinExistingDM : ElementCallIntent.JoinExistingDMVoice;
- }
- if (dm) {
- return video ? ElementCallIntent.StartCallDM : ElementCallIntent.StartCallDMVoice;
- }
-
- if (ongoing) {
- return video ? ElementCallIntent.JoinExisting : ElementCallIntent.JoinExistingVoice;
- }
- return video ? ElementCallIntent.StartCall : ElementCallIntent.StartCallVoice;
- }
-
- static dmCall(intent: ElementCallIntent): boolean {
- return (
- intent === ElementCallIntent.JoinExistingDM ||
- intent === ElementCallIntent.JoinExistingDMVoice ||
- intent === ElementCallIntent.StartCallDM ||
- intent === ElementCallIntent.StartCallDMVoice
- );
- }
-
- static startingCall(intent: ElementCallIntent): boolean {
- return (
- intent === ElementCallIntent.StartCallDM ||
- intent === ElementCallIntent.StartCallDMVoice ||
- intent === ElementCallIntent.StartCall ||
- intent === ElementCallIntent.StartCallVoice
- );
- }
-
- static getWidget(
- mx: MatrixClient,
- room: Room,
- intent: ElementCallIntent,
- themeKind: ElementCallThemeKind
- ): Widget {
- const userId = mx.getSafeUserId();
- const deviceId = mx.getDeviceId() ?? '';
- const clientOrigin = window.location.origin;
- const widgetId = 'call-embed';
-
- const params = new URLSearchParams({
- widgetId,
- parentUrl: clientOrigin,
- baseUrl: mx.baseUrl,
- roomId: room.roomId,
- userId,
- deviceId,
- intent,
-
- skipLobby: 'true',
- confineToRoom: 'true',
- appPrompt: 'false',
- perParticipantE2EE: room.hasEncryptionStateEvent().toString(),
- lang: 'en-EN',
- theme: themeKind,
- header: 'none',
- });
-
- if (!room.isCallRoom() && CallEmbed.startingCall(intent)) {
- params.append('sendNotificationType', CallEmbed.dmCall(intent) ? 'ring' : 'notification');
- }
-
- const widgetUrl = new URL(
- `${trimTrailingSlash(import.meta.env.BASE_URL)}/public/element-call/index.html`,
- window.location.origin
- );
- widgetUrl.search = params.toString();
-
- const options: IWidget = {
- id: widgetId,
- creatorUserId: userId,
- name: 'Call',
- type: 'm.call',
- url: widgetUrl.href,
- waitForIframeLoad: false,
- data: {},
- };
-
- const widget: Widget = new Widget(options);
-
- return widget;
- }
-
- static getIframe(url: string): HTMLIFrameElement {
- const iframe = document.createElement('iframe');
-
- iframe.title = 'Call Embed';
- iframe.sandbox =
- 'allow-forms allow-scripts allow-same-origin allow-popups allow-modals allow-downloads';
- iframe.allow = 'microphone; camera; display-capture; autoplay; clipboard-write;';
- iframe.src = url;
-
- iframe.style.width = '100%';
- iframe.style.height = '100%';
- iframe.style.border = 'none';
-
- return iframe;
- }
-
- constructor(
- mx: MatrixClient,
- room: Room,
- widget: Widget,
- container: HTMLElement,
- initialControlState?: CallControlState
- ) {
- const iframe = CallEmbed.getIframe(
- widget.getCompleteUrl({ currentUserId: mx.getSafeUserId() })
- );
- container.append(iframe);
-
- const callWidgetDriver: WidgetDriver = new CallWidgetDriver(mx, room.roomId);
- const call: ClientWidgetApi = new ClientWidgetApi(widget, iframe, callWidgetDriver);
-
- this.mx = mx;
- this.call = call;
- this.room = room;
- this.iframe = iframe;
- this.container = container;
-
- const controlState = initialControlState ?? new CallControlState(true, false, true);
- this.control = new CallControl(controlState, call, iframe);
- this.control.startObserving();
- iframe.onload = () => {
- this.control.startObserving();
- };
-
- let initialMediaEvent = true;
- this.disposables.push(
- this.listenAction(ElementWidgetActions.DeviceMute, (evt) => {
- if (initialMediaEvent) {
- initialMediaEvent = false;
- this.control.applyState();
- return;
- }
- this.control.onMediaState(evt);
- })
- );
-
- this.start();
- }
-
- get roomId(): string {
- return this.room.roomId;
- }
-
- get document(): Document | undefined {
- return this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
- }
-
- public setTheme(theme: ElementCallThemeKind) {
- return this.call.transport.send(WidgetApiToWidgetAction.ThemeChange, {
- name: theme,
- });
- }
-
- public hangup() {
- return this.call.transport.send(ElementWidgetActions.HangupCall, {});
- }
-
- public onPreparing(callback: () => void) {
- return this.listenEvent('preparing', callback);
- }
-
- public onPreparingError(callback: (error: any) => void) {
- return this.listenEvent('error:preparing', callback);
- }
-
- public onReady(callback: () => void) {
- return this.listenEvent('ready', callback);
- }
-
- public onCapabilitiesNotified(callback: () => void) {
- return this.listenEvent('capabilitiesNotified', callback);
- }
-
- private start() {
- // Room widgets get locked to the room they were added in
- this.call.setViewedRoomId(this.roomId);
- this.disposables.push(
- this.listenAction(ElementWidgetActions.JoinCall, this.onCallJoined.bind(this))
- );
-
- // Populate the map of "read up to" events for this widget with the current event in every room.
- // This is a bit inefficient, but should be okay. We do this for all rooms in case the widget
- // requests timeline capabilities in other rooms down the road. It's just easier to manage here.
- this.mx.getRooms().forEach((room) => {
- // Timelines are most recent last
- const events = room.getLiveTimeline()?.getEvents() || [];
- const roomEvent = events[events.length - 1];
- if (!roomEvent) return; // force later code to think the room is fresh
- this.readUpToMap[room.roomId] = roomEvent.getId()!;
- });
-
- // Attach listeners for feeding events - the underlying widget classes handle permissions for us
- this.mx.on(ClientEvent.Event, this.onEvent.bind(this));
- this.mx.on(MatrixEventEvent.Decrypted, this.onEventDecrypted.bind(this));
- this.mx.on(RoomStateEvent.Events, this.onStateUpdate.bind(this));
- this.mx.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent.bind(this));
- }
-
- /**
- * Stops the widget messaging for if it is started. Skips stopping if it is an active
- * widget.
- * @param opts
- */
- public dispose(): void {
- this.disposables.forEach((disposable) => {
- disposable();
- });
- this.call.stop();
- this.container.removeChild(this.iframe);
- this.control.dispose();
-
- this.mx.off(ClientEvent.Event, this.onEvent.bind(this));
- this.mx.off(MatrixEventEvent.Decrypted, this.onEventDecrypted.bind(this));
- this.mx.off(RoomStateEvent.Events, this.onStateUpdate.bind(this));
- this.mx.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent.bind(this));
-
- // Clear internal state
- this.readUpToMap = {};
- this.eventsToFeed = new WeakSet();
- }
-
- private onCallJoined(): void {
- this.joined = true;
- }
-
- private onEvent(ev: MatrixEvent): void {
- this.mx.decryptEventIfNeeded(ev);
- this.feedEvent(ev);
- }
-
- private onEventDecrypted(ev: MatrixEvent): void {
- this.feedEvent(ev);
- }
-
- private onStateUpdate(ev: MatrixEvent): void {
- if (this.call === null) return;
- const raw = ev.getEffectiveEvent();
- this.call.feedStateUpdate(raw as IRoomEvent).catch((e) => {
- console.error('Error sending state update to widget: ', e);
- });
- }
-
- private async onToDeviceEvent(ev: MatrixEvent): Promise {
- await this.mx.decryptEventIfNeeded(ev);
- if (ev.isDecryptionFailure()) return;
- await this.call?.feedToDevice(ev.getEffectiveEvent() as IRoomEvent, ev.isEncrypted());
- }
-
- /**
- * Determines whether the event has a relation to an unknown parent.
- */
- private relatesToUnknown(ev: MatrixEvent): boolean {
- // Replies to unknown events don't count
- if (!ev.relationEventId || ev.replyEventId) return false;
- const room = this.mx.getRoom(ev.getRoomId());
- return room === null || !room.findEventById(ev.relationEventId);
- }
-
- /**
- * Advances the "read up to" marker for a room to a certain event. No-ops if
- * the event is before the marker.
- * @returns Whether the "read up to" marker was advanced.
- */
- private advanceReadUpToMarker(ev: MatrixEvent): boolean {
- const evId = ev.getId();
- if (evId === undefined) return false;
- const roomId = ev.getRoomId();
- if (roomId === undefined) return false;
- const room = this.mx.getRoom(roomId);
- if (room === null) return false;
-
- const upToEventId = this.readUpToMap[ev.getRoomId()!];
- if (!upToEventId) {
- // There's no marker yet; start it at this event
- this.readUpToMap[roomId] = evId;
- return true;
- }
-
- // Small optimization for exact match (skip the search)
- if (upToEventId === evId) return false;
-
- // Timelines are most recent last, so reverse the order and limit ourselves to 100 events
- // to avoid overusing the CPU.
- const timeline = room.getLiveTimeline();
- const events = [...timeline.getEvents()].reverse().slice(0, 100);
- function isRelevantTimelineEvent(timelineEvent: MatrixEvent): boolean {
- return timelineEvent.getId() === upToEventId || timelineEvent.getId() === ev.getId();
- }
- const possibleMarkerEv = events.find(isRelevantTimelineEvent);
- if (possibleMarkerEv?.getId() === upToEventId) {
- // The event must be somewhere before the "read up to" marker
- return false;
- }
- if (possibleMarkerEv?.getId() === ev.getId()) {
- // The event is after the marker; advance it
- this.readUpToMap[roomId] = evId;
- return true;
- }
-
- // We can't say for sure whether the widget has seen the event; let's
- // just assume that it has
- return false;
- }
-
- /**
- * Determines whether the event comes from a room that we've been invited to
- * (in which case we likely don't have the full timeline).
- */
- private isFromInvite(ev: MatrixEvent): boolean {
- const room = this.mx.getRoom(ev.getRoomId());
- return room?.getMyMembership() === KnownMembership.Invite;
- }
-
- private feedEvent(ev: MatrixEvent): void {
- if (this.call === null) return;
- if (
- // If we had decided earlier to feed this event to the widget, but
- // it just wasn't ready, give it another try
- this.eventsToFeed.delete(ev) ||
- // Skip marker timeline check for events with relations to unknown parent because these
- // events are not added to the timeline here and will be ignored otherwise:
- // https://github.com/matrix-org/matrix-js-sdk/blob/d3dfcd924201d71b434af3d77343b5229b6ed75e/src/models/room.ts#L2207-L2213
- this.relatesToUnknown(ev) ||
- // Skip marker timeline check for rooms where membership is
- // 'invite', otherwise the membership event from the invitation room
- // will advance the marker and new state events will not be
- // forwarded to the widget.
- this.isFromInvite(ev) ||
- // Check whether this event would be before or after our "read up to" marker. If it's
- // before, or we can't decide, then we assume the widget will have already seen the event.
- // If the event is after, or we don't have a marker for the room,
- // then the marker will advance and we'll send it through.
- // This approach of "read up to" prevents widgets receiving decryption spam from startup or
- // receiving ancient events from backfill and such.
- this.advanceReadUpToMarker(ev)
- ) {
- // If the event is still being decrypted, remember that we want to
- // feed it to the widget (even if not strictly in the order given by
- // the timeline) and get back to it later
- if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) {
- this.eventsToFeed.add(ev);
- } else {
- const raw = ev.getEffectiveEvent();
- this.call.feedEvent(raw as IRoomEvent).catch((e) => {
- console.error('Error sending event to widget: ', e);
- });
- }
- }
- }
-
- public listenAction(type: string, callback: (event: CustomEvent) => void) {
- return this.listenEvent(`action:${type}`, callback);
- }
-
- public listenEvent(type: string, callback: (event: T) => void) {
- this.call.on(type, callback);
- return () => {
- this.call.off(type, callback);
- };
- }
-}
diff --git a/src/app/plugins/call/CallWidgetDriver.ts b/src/app/plugins/call/CallWidgetDriver.ts
deleted file mode 100644
index d21eaa7b..00000000
--- a/src/app/plugins/call/CallWidgetDriver.ts
+++ /dev/null
@@ -1,325 +0,0 @@
-import {
- type Capability,
- type ISendDelayedEventDetails,
- type ISendEventDetails,
- type IReadEventRelationsResult,
- type IRoomEvent,
- WidgetDriver,
- type IWidgetApiErrorResponseDataDetails,
- type ISearchUserDirectoryResult,
- type IGetMediaConfigResult,
- OpenIDRequestState,
- SimpleObservable,
- IOpenIDUpdate,
-} from 'matrix-widget-api';
-import {
- EventType,
- type IContent,
- MatrixError,
- type MatrixEvent,
- Direction,
- type SendDelayedEventResponse,
- type StateEvents,
- type TimelineEvents,
- MatrixClient,
-} from 'matrix-js-sdk';
-import { getCallCapabilities } from './utils';
-import { downloadMedia, mxcUrlToHttp } from '../../utils/matrix';
-
-export class CallWidgetDriver extends WidgetDriver {
- private allowedCapabilities: Set;
-
- private readonly mx: MatrixClient;
-
- public constructor(mx: MatrixClient, private inRoomId: string) {
- super();
- this.mx = mx;
-
- const deviceId = mx.getDeviceId();
- if (!deviceId) throw new Error('Failed to initialize CallWidgetDriver! Device ID not found.');
-
- this.allowedCapabilities = getCallCapabilities(inRoomId, mx.getSafeUserId(), deviceId);
- }
-
- public async validateCapabilities(requested: Set): Promise> {
- const allow = Array.from(requested).filter((cap) => this.allowedCapabilities.has(cap));
- return new Set(allow);
- }
-
- public async sendEvent(
- eventType: string,
- content: IContent,
- stateKey: string | null = null,
- targetRoomId: string | null = null
- ): Promise {
- const roomId = targetRoomId || this.inRoomId;
-
- let r: { event_id: string } | null;
- if (typeof stateKey === 'string') {
- r = await this.mx.sendStateEvent(
- roomId,
- eventType as keyof StateEvents,
- content as StateEvents[keyof StateEvents],
- stateKey
- );
- } else if (eventType === EventType.RoomRedaction) {
- // special case: extract the `redacts` property and call redact
- r = await this.mx.redactEvent(roomId, content.redacts);
- } else {
- r = await this.mx.sendEvent(
- roomId,
- eventType as keyof TimelineEvents,
- content as TimelineEvents[keyof TimelineEvents]
- );
- }
-
- return { roomId, eventId: r.event_id };
- }
-
- public async sendDelayedEvent(
- delay: number | null,
- parentDelayId: string | null,
- eventType: string,
- content: IContent,
- stateKey: string | null = null,
- targetRoomId: string | null = null
- ): Promise {
- const roomId = targetRoomId || this.inRoomId;
-
- let delayOpts;
- if (delay !== null) {
- delayOpts = {
- delay,
- ...(parentDelayId !== null && { parent_delay_id: parentDelayId }),
- };
- } else if (parentDelayId !== null) {
- delayOpts = {
- parent_delay_id: parentDelayId,
- };
- } else {
- throw new Error('Must provide at least one of delay or parentDelayId');
- }
-
- let r: SendDelayedEventResponse | null;
- if (stateKey !== null) {
- // state event
- r = await this.mx._unstable_sendDelayedStateEvent(
- roomId,
- delayOpts,
- eventType as keyof StateEvents,
- content as StateEvents[keyof StateEvents],
- stateKey
- );
- } else {
- // message event
- r = await this.mx._unstable_sendDelayedEvent(
- roomId,
- delayOpts,
- null,
- eventType as keyof TimelineEvents,
- content as TimelineEvents[keyof TimelineEvents]
- );
- }
-
- return {
- roomId,
- delayId: r.delay_id,
- };
- }
-
- public async cancelScheduledDelayedEvent(delayId: string): Promise {
- await this.mx._unstable_cancelScheduledDelayedEvent(delayId);
- }
-
- public async restartScheduledDelayedEvent(delayId: string): Promise {
- await this.mx._unstable_restartScheduledDelayedEvent(delayId);
- }
-
- public async sendScheduledDelayedEvent(delayId: string): Promise {
- await this.mx._unstable_sendScheduledDelayedEvent(delayId);
- }
-
- public async sendToDevice(
- eventType: string,
- encrypted: boolean,
- contentMap: { [userId: string]: { [deviceId: string]: object } }
- ): Promise {
- if (encrypted) {
- const crypto = this.mx.getCrypto();
- if (!crypto) throw new Error('E2EE not enabled');
-
- // attempt to re-batch these up into a single request
- const invertedContentMap: { [content: string]: { userId: string; deviceId: string }[] } = {};
-
- // eslint-disable-next-line no-restricted-syntax
- for (const userId of Object.keys(contentMap)) {
- const userContentMap = contentMap[userId];
- // eslint-disable-next-line no-restricted-syntax
- for (const deviceId of Object.keys(userContentMap)) {
- const content = userContentMap[deviceId];
- const stringifiedContent = JSON.stringify(content);
- invertedContentMap[stringifiedContent] = invertedContentMap[stringifiedContent] || [];
- invertedContentMap[stringifiedContent].push({ userId, deviceId });
- }
- }
-
- await Promise.all(
- Object.entries(invertedContentMap).map(async ([stringifiedContent, recipients]) => {
- const batch = await crypto.encryptToDeviceMessages(
- eventType,
- recipients,
- JSON.parse(stringifiedContent)
- );
-
- await this.mx.queueToDevice(batch);
- })
- );
- } else {
- await this.mx.queueToDevice({
- eventType,
- batch: Object.entries(contentMap).flatMap(([userId, userContentMap]) =>
- Object.entries(userContentMap).map(([deviceId, content]) => ({
- userId,
- deviceId,
- payload: content,
- }))
- ),
- });
- }
- }
-
- public async readRoomTimeline(
- roomId: string,
- eventType: string,
- msgtype: string | undefined,
- stateKey: string | undefined,
- limit: number,
- since: string | undefined
- ): Promise {
- const safeLimit =
- limit > 0 ? Math.min(limit, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary
-
- const room = this.mx.getRoom(roomId);
- if (room === null) return [];
- const results: MatrixEvent[] = [];
- const events = room.getLiveTimeline().getEvents();
-
- for (let i = events.length - 1; i >= 0; i -= 1) {
- const ev = events[i];
- if (results.length >= safeLimit) break;
- if (since !== undefined && ev.getId() === since) break;
-
- if (
- ev.getType() === eventType &&
- !ev.isState() &&
- (eventType !== EventType.RoomMessage || !msgtype || msgtype === ev.getContent().msgtype) &&
- (ev.getStateKey() === undefined || stateKey === undefined || ev.getStateKey() === stateKey)
- ) {
- results.push(ev);
- }
- }
-
- return results.map((e) => e.getEffectiveEvent() as IRoomEvent);
- }
-
- public async askOpenID(observer: SimpleObservable): Promise {
- return observer.update({
- state: OpenIDRequestState.Allowed,
- token: await this.mx.getOpenIdToken(),
- });
- }
-
- public async readRoomState(
- roomId: string,
- eventType: string,
- stateKey: string | undefined
- ): Promise {
- const room = this.mx.getRoom(roomId);
- if (room === null) return [];
- const state = room.getLiveTimeline().getState(Direction.Forward);
- if (state === undefined) return [];
-
- if (stateKey === undefined)
- return state.getStateEvents(eventType).map((e) => e.getEffectiveEvent() as IRoomEvent);
- const event = state.getStateEvents(eventType, stateKey);
- return event === null ? [] : [event.getEffectiveEvent() as IRoomEvent];
- }
-
- public async readEventRelations(
- eventId: string,
- roomId?: string,
- relationType?: string,
- eventType?: string,
- from?: string,
- to?: string,
- limit?: number,
- direction?: 'f' | 'b'
- ): Promise {
- const dir = direction as Direction;
- const targetRoomId = roomId ?? this.inRoomId ?? undefined;
-
- if (typeof targetRoomId !== 'string') {
- throw new Error('Error while reading the current room');
- }
-
- const { events, nextBatch, prevBatch } = await this.mx.relations(
- targetRoomId,
- eventId,
- relationType ?? null,
- eventType ?? null,
- { from, to, limit, dir }
- );
-
- return {
- chunk: events.map((e) => e.getEffectiveEvent() as IRoomEvent),
- nextBatch: nextBatch ?? undefined,
- prevBatch: prevBatch ?? undefined,
- };
- }
-
- public async searchUserDirectory(
- searchTerm: string,
- limit?: number
- ): Promise {
- const { limited, results } = await this.mx.searchUserDirectory({ term: searchTerm, limit });
-
- return {
- limited,
- results: results.map((r) => ({
- userId: r.user_id,
- displayName: r.display_name,
- avatarUrl: r.avatar_url,
- })),
- };
- }
-
- public async getMediaConfig(): Promise {
- return this.mx.getMediaConfig();
- }
-
- public async uploadFile(file: XMLHttpRequestBodyInit): Promise<{ contentUri: string }> {
- const uploadResult = await this.mx.uploadContent(file);
-
- return { contentUri: uploadResult.content_uri };
- }
-
- public async downloadFile(contentUri: string): Promise<{ file: XMLHttpRequestBodyInit }> {
- const httpUrl = mxcUrlToHttp(this.mx, contentUri, true);
- if (!httpUrl) {
- throw new Error('Call widget failed to download file! No http url!');
- }
- const blob = await downloadMedia(httpUrl);
- return { file: blob };
- }
-
- public getKnownRooms(): string[] {
- return this.mx.getVisibleRooms().map((r) => r.roomId);
- }
-
- // eslint-disable-next-line class-methods-use-this
- public processError(error: unknown): IWidgetApiErrorResponseDataDetails | undefined {
- return error instanceof MatrixError
- ? { matrix_api_error: error.asWidgetApiErrorData() }
- : undefined;
- }
-}
diff --git a/src/app/plugins/call/hooks.ts b/src/app/plugins/call/hooks.ts
deleted file mode 100644
index f5b3ff93..00000000
--- a/src/app/plugins/call/hooks.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import {
- ClientWidgetApi,
- IWidgetApiAcknowledgeResponseData,
- IWidgetApiRequestData,
-} from 'matrix-widget-api';
-import { useCallback, useEffect, useState } from 'react';
-import { CallControl, CallControlEvent } from './CallControl';
-import { CallControlState } from './CallControlState';
-
-export const useClientWidgetApiEvent = (
- api: ClientWidgetApi | undefined,
- type: string,
- callback: (event: CustomEvent) => void
-) => {
- useEffect(() => {
- api?.on(`action:${type}`, callback);
- return () => {
- api?.off(`action:${type}`, callback);
- };
- }, [api, type, callback]);
-};
-
-export const useSendClientWidgetApiAction = (api: ClientWidgetApi) => {
- const sendWidgetAction = useCallback(
- async (
- action: string,
- data: T
- ): Promise => api.transport.send(action, data),
- [api]
- );
-
- return sendWidgetAction;
-};
-
-export const useCallControlState = (control: CallControl): CallControlState => {
- const [state, setState] = useState(control.getState());
-
- useEffect(() => {
- const handleUpdate = () => {
- setState(control.getState());
- };
- control.on(CallControlEvent.StateUpdate, handleUpdate);
- return () => {
- control.off(CallControlEvent.StateUpdate, handleUpdate);
- };
- }, [control]);
-
- return state;
-};
diff --git a/src/app/plugins/call/index.ts b/src/app/plugins/call/index.ts
deleted file mode 100644
index a4c6bb36..00000000
--- a/src/app/plugins/call/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export * from './CallEmbed';
-export * from './hooks';
-export * from './types';
diff --git a/src/app/plugins/call/types.ts b/src/app/plugins/call/types.ts
deleted file mode 100644
index 89a9e61d..00000000
--- a/src/app/plugins/call/types.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-export enum ElementCallIntent {
- StartCall = 'start_call',
- JoinExisting = 'join_existing',
- StartCallVoice = 'start_call_voice',
- JoinExistingVoice = 'join_existing_voice',
- StartCallDM = 'start_call_dm',
- JoinExistingDM = 'join_existing_dm',
- StartCallDMVoice = 'start_call_dm_voice',
- JoinExistingDMVoice = 'join_existing_dm_voice',
-}
-
-export type ElementCallThemeKind = 'light' | 'dark';
-
-export type ElementMediaStatePayload = {
- audio_enabled?: boolean;
- video_enabled?: boolean;
-};
-export type ElementMediaStateDetail = {
- data?: ElementMediaStatePayload;
-};
-
-export enum ElementWidgetActions {
- JoinCall = 'io.element.join',
- HangupCall = 'im.vector.hangup',
- Close = 'io.element.close',
- DeviceMute = 'io.element.device_mute',
-}
diff --git a/src/app/plugins/call/utils.ts b/src/app/plugins/call/utils.ts
deleted file mode 100644
index 58d6d967..00000000
--- a/src/app/plugins/call/utils.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import {
- type Capability,
- EventDirection,
- MatrixCapabilities,
- WidgetEventCapability,
-} from 'matrix-widget-api';
-import { EventType } from 'matrix-js-sdk';
-
-export function getCallCapabilities(
- roomId: string,
- userId: string,
- deviceId: string
-): Set {
- const capabilities: Set = new Set();
-
- capabilities.add(MatrixCapabilities.Screenshots);
- capabilities.add(MatrixCapabilities.AlwaysOnScreen);
- capabilities.add(MatrixCapabilities.MSC4039UploadFile);
- capabilities.add(MatrixCapabilities.MSC4039DownloadFile);
- capabilities.add(MatrixCapabilities.MSC3846TurnServers);
- capabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent);
- capabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent);
- capabilities.add(`org.matrix.msc2762.timeline:${roomId}`);
- capabilities.add(`org.matrix.msc2762.state:${roomId}`);
-
- capabilities.add(
- WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomMember).raw
- );
- capabilities.add(
- WidgetEventCapability.forStateEvent(EventDirection.Receive, 'org.matrix.msc3401.call').raw
- );
- capabilities.add(
- WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomEncryption).raw
- );
- capabilities.add(
- WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomName).raw
- );
-
- capabilities.add(
- WidgetEventCapability.forStateEvent(
- EventDirection.Send,
- 'org.matrix.msc3401.call.member',
- userId
- ).raw
- );
- capabilities.add(
- WidgetEventCapability.forStateEvent(
- EventDirection.Send,
- 'org.matrix.msc3401.call.member',
- `_${userId}_${deviceId}_m.call`
- ).raw
- );
- capabilities.add(
- WidgetEventCapability.forStateEvent(
- EventDirection.Send,
- 'org.matrix.msc3401.call.member',
- `${userId}_${deviceId}_m.call`
- ).raw
- );
- capabilities.add(
- WidgetEventCapability.forStateEvent(
- EventDirection.Send,
- 'org.matrix.msc3401.call.member',
- `_${userId}_${deviceId}`
- ).raw
- );
- capabilities.add(
- WidgetEventCapability.forStateEvent(
- EventDirection.Send,
- 'org.matrix.msc3401.call.member',
- `${userId}_${deviceId}`
- ).raw
- );
-
- capabilities.add(
- WidgetEventCapability.forStateEvent(EventDirection.Receive, 'org.matrix.msc3401.call.member')
- .raw
- );
- capabilities.add(
- WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomCreate).raw
- );
-
- [
- 'io.element.call.encryption_keys',
- 'org.matrix.rageshake_request',
- EventType.Reaction,
- EventType.RoomRedaction,
- 'io.element.call.reaction',
- 'org.matrix.msc4075.rtc.notification',
- 'org.matrix.msc4310.rtc.decline',
- ].forEach((type) => {
- capabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Send, type).raw);
- capabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Receive, type).raw);
- });
-
- [
- EventType.CallInvite,
- EventType.CallCandidates,
- EventType.CallAnswer,
- EventType.CallHangup,
- EventType.CallReject,
- EventType.CallSelectAnswer,
- EventType.CallNegotiate,
- EventType.CallSDPStreamMetadataChanged,
- EventType.CallSDPStreamMetadataChangedPrefix,
- EventType.CallReplaces,
- EventType.CallEncryptionKeysPrefix,
- ].forEach((type) => {
- capabilities.add(WidgetEventCapability.forToDeviceEvent(EventDirection.Send, type).raw);
- capabilities.add(WidgetEventCapability.forToDeviceEvent(EventDirection.Receive, type).raw);
- });
-
- return capabilities;
-}
diff --git a/src/app/plugins/markdown/block/parser.ts b/src/app/plugins/markdown/block/parser.ts
index b4aa1c3d..ed16a327 100644
--- a/src/app/plugins/markdown/block/parser.ts
+++ b/src/app/plugins/markdown/block/parser.ts
@@ -1,5 +1,12 @@
import { replaceMatch } from '../internal';
-import { BlockQuoteRule, CodeBlockRule, ESC_BLOCK_SEQ, HeadingRule, ListRule } from './rules';
+import {
+ BlockQuoteRule,
+ CodeBlockRule,
+ ESC_BLOCK_SEQ,
+ HeadingRule,
+ OrderedListRule,
+ UnorderedListRule,
+} from './rules';
import { runBlockRule } from './runner';
import { BlockMDParser } from './type';
@@ -16,7 +23,8 @@ export const parseBlockMD: BlockMDParser = (text, parseInline) => {
if (!result) result = runBlockRule(text, CodeBlockRule, parseBlockMD, parseInline);
if (!result) result = runBlockRule(text, BlockQuoteRule, parseBlockMD, parseInline);
- if (!result) result = runBlockRule(text, ListRule, parseBlockMD, parseInline);
+ if (!result) result = runBlockRule(text, OrderedListRule, parseBlockMD, parseInline);
+ if (!result) result = runBlockRule(text, UnorderedListRule, parseBlockMD, parseInline);
if (!result) result = runBlockRule(text, HeadingRule, parseBlockMD, parseInline);
// replace \n with because want to preserve empty lines
diff --git a/src/app/plugins/markdown/block/rules.ts b/src/app/plugins/markdown/block/rules.ts
index 231397cf..f598ee63 100644
--- a/src/app/plugins/markdown/block/rules.ts
+++ b/src/app/plugins/markdown/block/rules.ts
@@ -10,22 +10,14 @@ export const HeadingRule: BlockMDRule = {
},
};
-// opening fence: 3 or more backticks
-// capture the exact fence length in group 1
-// optional info string in group 2
-// code content in group 3
-// closing fence must match the exact same fence sequence via \1
-const CODEBLOCK_REG_1 = /^(`{3,})(?!`)(\S*)\n((?:.*\n)+?)\1 *(?!.)\n?/m;
+const CODEBLOCK_MD_1 = '```';
+const CODEBLOCK_REG_1 = /^`{3}(\S*)\n((?:.*\n)+?)`{3} *(?!.)\n?/m;
export const CodeBlockRule: BlockMDRule = {
match: (text) => text.match(CODEBLOCK_REG_1),
html: (match) => {
- const [, fence, g1, g2] = match;
- // use last identifier after dot, e.g. for "example.json" gets us "json" as language code.
- const langCode = g1 ? g1.substring(g1.lastIndexOf('.') + 1) : null;
- const filename = g1 !== langCode ? g1 : null;
- const classNameAtt = langCode ? ` class="language-${langCode}"` : '';
- const filenameAtt = filename ? ` data-label="${filename}"` : '';
- return `${g2} `;
+ const [, g1, g2] = match;
+ const classNameAtt = g1 ? ` class="language-${g1}"` : '';
+ return `${g2} `;
},
};
@@ -52,146 +44,55 @@ export const BlockQuoteRule: BlockMDRule = {
};
const ORDERED_LIST_MD_1 = '-';
+const O_LIST_ITEM_PREFIX = /^(-|[\da-zA-Z]\.) */;
+const O_LIST_START = /^([\d])\./;
+const O_LIST_TYPE = /^([aAiI])\./;
+const O_LIST_TRAILING_NEWLINE = /\n$/;
+const ORDERED_LIST_REG_1 = /(^(?:-|[\da-zA-Z]\.) +.+\n?)+/m;
+export const OrderedListRule: BlockMDRule = {
+ match: (text) => text.match(ORDERED_LIST_REG_1),
+ html: (match, parseInline) => {
+ const [listText] = match;
+ const [, listStart] = listText.match(O_LIST_START) ?? [];
+ const [, listType] = listText.match(O_LIST_TYPE) ?? [];
+
+ const lines = listText
+ .replace(O_LIST_TRAILING_NEWLINE, '')
+ .split('\n')
+ .map((lineText) => {
+ const line = lineText.replace(O_LIST_ITEM_PREFIX, '');
+ const txt = parseInline ? parseInline(line) : line;
+ return `${txt}
`;
+ })
+ .join('');
+
+ const dataMdAtt = `data-md="${listType || listStart || ORDERED_LIST_MD_1}"`;
+ const startAtt = listStart ? ` start="${listStart}"` : '';
+ const typeAtt = listType ? ` type="${listType}"` : '';
+ return `${lines} `;
+ },
+};
+
const UNORDERED_LIST_MD_1 = '*';
-const LIST_ITEM_REG = /^( *)([-*]|[\da-zA-Z]\.) +(.+)$/;
-type ListType = 'ol' | 'ul';
-
-function getListType(marker: string): ListType {
- return marker === '*' ? 'ul' : 'ol';
-}
-
-function getOrderedMeta(marker: string) {
- const startMatch = marker.match(/^(\d)\./);
- const typeMatch = marker.match(/^([aAiI])\./);
-
- return {
- start: startMatch?.[1],
- type: typeMatch?.[1],
- };
-}
-
-interface ParsedLine {
- indent: number;
- marker: string;
- content: string;
- listType: ListType;
-}
-
-function parseLines(text: string): ParsedLine[] {
- return text
- .replace(/\n$/, '')
- .split('\n')
- .map((line) => {
- const match = line.match(LIST_ITEM_REG);
-
- if (!match) return null;
-
- const [, spaces, marker, content] = match;
-
- return {
- indent: spaces.length,
- marker,
- content,
- listType: getListType(marker),
- };
- })
- .filter(Boolean) as ParsedLine[];
-}
-
-function openList(line: ParsedLine) {
- if (line.listType === 'ul') {
- return ``;
- }
- const { type, start } = getOrderedMeta(line.marker);
- const dataMdAtt = `data-md="${type || start || ORDERED_LIST_MD_1}"`;
- const startAtt = start ? ` start="${start}"` : '';
- const typeAtt = type ? ` type="${type}"` : '';
- return ``;
-}
-
-function closeList(listType: ListType) {
- return listType === 'ul' ? ' ' : '';
-}
-
-function buildList(lines: ParsedLine[], parseInline?: (s: string) => string): string {
- let html = '';
-
- const stack: ('ul' | 'ol')[] = [];
-
- lines.forEach((line, index) => {
- const prev = lines[index - 1];
- const next = lines[index + 1];
-
- const content = parseInline ? parseInline(line.content) : line.content;
-
- // FIRST ITEM
- if (!prev) {
- html += openList(line);
- stack.push(line.listType);
- }
-
- // DEEPER INDENT > open nested list
- else if (line.indent > prev.indent) {
- html += openList(line);
- stack.push(line.listType);
- }
-
- // SAME LEVEL
- else if (line.indent === prev.indent) {
- html += '';
-
- // different list type
- if (line.listType !== prev.listType) {
- html += closeList(stack.pop()!);
-
- html += openList(line);
- stack.push(line.listType);
- }
- }
-
- // GOING BACK UP
- else if (line.indent < prev.indent) {
- html += '';
-
- while (stack.length > line.indent + 1) {
- html += closeList(stack.pop()!);
- html += '';
- }
-
- if (line.listType !== stack[stack.length - 1]) {
- html += closeList(stack.pop()!);
-
- html += openList(line);
- stack.push(line.listType);
- }
- }
-
- html += `${content}
`;
-
- // LAST ITEM cleanup
- if (!next) {
- html += ' ';
-
- while (stack.length) {
- html += closeList(stack.pop()!);
- }
- }
- });
-
- return html;
-}
-
-const LIST_REG_1 = /^(?: *(?:[-*]|[\da-zA-Z]\.) +.+\n?)+/m;
-export const ListRule: BlockMDRule = {
- match: (text) => text.match(LIST_REG_1),
+const U_LIST_ITEM_PREFIX = /^\* */;
+const U_LIST_TRAILING_NEWLINE = /\n$/;
+const UNORDERED_LIST_REG_1 = /(^\* +.+\n?)+/m;
+export const UnorderedListRule: BlockMDRule = {
+ match: (text) => text.match(UNORDERED_LIST_REG_1),
html: (match, parseInline) => {
const [listText] = match;
- const lines = parseLines(listText);
+ const lines = listText
+ .replace(U_LIST_TRAILING_NEWLINE, '')
+ .split('\n')
+ .map((lineText) => {
+ const line = lineText.replace(U_LIST_ITEM_PREFIX, '');
+ const txt = parseInline ? parseInline(line) : line;
+ return `${txt}
`;
+ })
+ .join('');
- const html = buildList(lines, parseInline);
-
- return html;
+ return ``;
},
};
diff --git a/src/app/plugins/matrix-to.ts b/src/app/plugins/matrix-to.ts
index 03a7d2c1..feeafe05 100644
--- a/src/app/plugins/matrix-to.ts
+++ b/src/app/plugins/matrix-to.ts
@@ -16,11 +16,11 @@ export const getMatrixToRoom = (roomIdOrAlias: string, viaServers?: string[]): s
};
export const getMatrixToRoomEvent = (
- roomId: string,
+ roomIdOrAlias: string,
eventId: string,
viaServers?: string[]
): string => {
- let fragment = `${roomId}/${eventId}`;
+ let fragment = `${roomIdOrAlias}/${eventId}`;
if (Array.isArray(viaServers) && viaServers.length > 0) {
fragment = withViaServers(fragment, viaServers);
diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx
index 961131d9..ba40c978 100644
--- a/src/app/plugins/react-custom-html-parser.tsx
+++ b/src/app/plugins/react-custom-html-parser.tsx
@@ -232,9 +232,8 @@ export function CodeBlock({
opts: HTMLReactParserOptions;
}) {
const code = children[0];
- const attribs = code instanceof Element && code.name === 'code' ? code.attribs : undefined;
- const languageClass = attribs?.class;
- const customLabel = attribs?.['data-label'];
+ const languageClass =
+ code instanceof Element && code.name === 'code' ? code.attribs.class : undefined;
const language =
languageClass && languageClass.startsWith('language-')
? languageClass.replace('language-', '')
@@ -263,7 +262,7 @@ export function CodeBlock({
- {customLabel ?? language ?? 'Code'}
+ {language ?? 'Code'}
diff --git a/src/app/plugins/recent-emoji.ts b/src/app/plugins/recent-emoji.ts
index 811ed9d5..3634538f 100644
--- a/src/app/plugins/recent-emoji.ts
+++ b/src/app/plugins/recent-emoji.ts
@@ -27,11 +27,7 @@ export const getRecentEmojis = (mx: MatrixClient, limit?: number): IEmoji[] => {
export function addRecentEmoji(mx: MatrixClient, unicode: string) {
const recentEmojiEvent = getAccountData(mx, AccountDataEvent.ElementRecentEmoji);
- const recentEmojiContent = recentEmojiEvent?.getContent();
- const recentEmoji =
- recentEmojiContent && Array.isArray(recentEmojiContent.recent_emoji)
- ? structuredClone(recentEmojiContent.recent_emoji)
- : [];
+ const recentEmoji = recentEmojiEvent?.getContent().recent_emoji ?? [];
const emojiIndex = recentEmoji.findIndex(([u]) => u === unicode);
let entry: [EmojiUnicode, EmojiUsageCount];
diff --git a/src/app/state/callEmbed.ts b/src/app/state/callEmbed.ts
deleted file mode 100644
index a9f89300..00000000
--- a/src/app/state/callEmbed.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { atom } from 'jotai';
-import { CallEmbed } from '../plugins/call';
-
-const baseCallEmbedAtom = atom(undefined);
-
-export const callEmbedAtom = atom(
- (get) => get(baseCallEmbedAtom),
- (get, set, callEmbed) => {
- const prevCallEmbed = get(baseCallEmbedAtom);
- if (callEmbed === prevCallEmbed) return;
-
- if (prevCallEmbed) {
- prevCallEmbed.dispose();
- }
-
- set(baseCallEmbedAtom, callEmbed);
- }
-);
-
-export const callChatAtom = atom(false);
diff --git a/src/app/state/callPreferences.ts b/src/app/state/callPreferences.ts
deleted file mode 100644
index a8e82067..00000000
--- a/src/app/state/callPreferences.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { WritableAtom } from 'jotai';
-import {
- atomWithLocalStorage,
- getLocalStorageItem,
- setLocalStorageItem,
-} from './utils/atomWithLocalStorage';
-
-export type CallPreferences = {
- microphone: boolean;
- video: boolean;
- sound: boolean;
-};
-
-const CALL_PREFERENCES = 'callPreferences';
-
-const DEFAULT_PREFERENCES: CallPreferences = {
- microphone: true,
- video: false,
- sound: true,
-};
-
-export type CallPreferencesAtom = WritableAtom;
-
-export const makeCallPreferencesAtom = (userId: string): CallPreferencesAtom => {
- const storeKey = `${CALL_PREFERENCES}${userId}`;
-
- const callPreferencesAtom = atomWithLocalStorage(
- storeKey,
- (key) => {
- const v = getLocalStorageItem(key, DEFAULT_PREFERENCES);
- return v;
- },
- (key, value) => {
- setLocalStorageItem(key, value);
- }
- );
-
- return callPreferencesAtom;
-};
diff --git a/src/app/state/createRoomModal.ts b/src/app/state/createRoomModal.ts
index f32e7665..81af5d5b 100644
--- a/src/app/state/createRoomModal.ts
+++ b/src/app/state/createRoomModal.ts
@@ -1,9 +1,7 @@
import { atom } from 'jotai';
-import { CreateRoomType } from '../components/create-room/types';
export type CreateRoomModalState = {
spaceId?: string;
- type?: CreateRoomType;
};
export const createRoomModalAtom = atom(undefined);
diff --git a/src/app/state/hooks/callPreferences.ts b/src/app/state/hooks/callPreferences.ts
deleted file mode 100644
index 829ed4b4..00000000
--- a/src/app/state/hooks/callPreferences.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { createContext, useCallback, useContext } from 'react';
-import { useAtom } from 'jotai';
-import { CallPreferences, CallPreferencesAtom } from '../callPreferences';
-
-const CallPreferencesAtomContext = createContext(null);
-export const CallPreferencesProvider = CallPreferencesAtomContext.Provider;
-
-export const useCallPreferencesAtom = (): CallPreferencesAtom => {
- const atom = useContext(CallPreferencesAtomContext);
- if (!atom) {
- throw new Error('CallPreferencesAtom not provided!');
- }
-
- return atom;
-};
-
-export const useCallPreferences = (): CallPreferences & {
- toggleMicrophone: () => void;
- toggleVideo: () => void;
- toggleSound: () => void;
-} => {
- const callPrefAtom = useCallPreferencesAtom();
- const [pref, setPref] = useAtom(callPrefAtom);
-
- const toggleMicrophone = useCallback(() => {
- const microphone = !pref.microphone;
-
- setPref({
- microphone,
- video: pref.video,
- sound: !pref.sound && microphone ? true : pref.sound,
- });
- }, [setPref, pref]);
-
- const toggleVideo = useCallback(() => {
- const video = !pref.video;
-
- setPref({
- microphone: pref.microphone,
- video,
- sound: pref.sound,
- });
- }, [setPref, pref]);
-
- const toggleSound = useCallback(() => {
- const sound = !pref.sound;
-
- setPref({
- microphone: !sound ? false : pref.microphone,
- video: pref.video,
- sound,
- });
- }, [setPref, pref]);
-
- return {
- ...pref,
- toggleMicrophone,
- toggleVideo,
- toggleSound,
- };
-};
diff --git a/src/app/state/hooks/createRoomModal.ts b/src/app/state/hooks/createRoomModal.ts
index 2663e5dc..15db7289 100644
--- a/src/app/state/hooks/createRoomModal.ts
+++ b/src/app/state/hooks/createRoomModal.ts
@@ -1,7 +1,6 @@
import { useCallback } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { createRoomModalAtom, CreateRoomModalState } from '../createRoomModal';
-import { CreateRoomType } from '../../components/create-room/types';
export const useCreateRoomModalState = (): CreateRoomModalState | undefined => {
const data = useAtomValue(createRoomModalAtom);
@@ -20,13 +19,13 @@ export const useCloseCreateRoomModal = (): CloseCallback => {
return close;
};
-type OpenCallback = (space?: string, type?: CreateRoomType) => void;
+type OpenCallback = (space?: string) => void;
export const useOpenCreateRoomModal = (): OpenCallback => {
const setSettings = useSetAtom(createRoomModalAtom);
const open: OpenCallback = useCallback(
- (spaceId, type) => {
- setSettings({ spaceId, type });
+ (spaceId) => {
+ setSettings({ spaceId });
},
[setSettings]
);
diff --git a/src/app/state/hooks/roomToActiveThread.ts b/src/app/state/hooks/roomToActiveThread.ts
new file mode 100644
index 00000000..7abb5974
--- /dev/null
+++ b/src/app/state/hooks/roomToActiveThread.ts
@@ -0,0 +1,55 @@
+import { createContext, useCallback, useContext } from 'react';
+import { useAtomValue, useSetAtom } from 'jotai';
+import { RoomToActiveThreadAtom } from '../roomToActiveThread';
+
+const RoomToActiveThreadAtomContext = createContext(null);
+export const RoomToActiveThreadProvider = RoomToActiveThreadAtomContext.Provider;
+
+export const useRoomToActiveThreadAtom = (): RoomToActiveThreadAtom => {
+ const anAtom = useContext(RoomToActiveThreadAtomContext);
+
+ if (!anAtom) {
+ throw new Error('RoomToActiveThreadAtom is not provided!');
+ }
+
+ return anAtom;
+};
+
+export const useThreadSelector = (roomId: string): ((threadId: string) => void) => {
+ const roomToActiveThreadAtom = useRoomToActiveThreadAtom();
+ const setRoomToActiveThread = useSetAtom(roomToActiveThreadAtom);
+
+ const onThreadSelect = useCallback(
+ (threadId: string) => {
+ setRoomToActiveThread({
+ type: 'PUT',
+ roomId,
+ threadId,
+ });
+ },
+ [roomId, setRoomToActiveThread]
+ );
+
+ return onThreadSelect;
+};
+
+export const useActiveThread = (roomId: string): string | undefined => {
+ const roomToActiveThreadAtom = useRoomToActiveThreadAtom();
+ const roomToActiveThread = useAtomValue(roomToActiveThreadAtom);
+
+ return roomToActiveThread.get(roomId);
+};
+
+export const useThreadClose = (roomId: string): (() => void) => {
+ const roomToActiveThreadAtom = useRoomToActiveThreadAtom();
+ const setRoomToActiveThread = useSetAtom(roomToActiveThreadAtom);
+
+ const closeThread = useCallback(() => {
+ setRoomToActiveThread({
+ type: 'DELETE',
+ roomId,
+ });
+ }, [roomId, setRoomToActiveThread]);
+
+ return closeThread;
+};
diff --git a/src/app/state/roomToActiveThread.ts b/src/app/state/roomToActiveThread.ts
new file mode 100644
index 00000000..e156c63a
--- /dev/null
+++ b/src/app/state/roomToActiveThread.ts
@@ -0,0 +1,75 @@
+import { atom, WritableAtom } from 'jotai';
+import produce from 'immer';
+import {
+ atomWithLocalStorage,
+ getLocalStorageItem,
+ setLocalStorageItem,
+} from './utils/atomWithLocalStorage';
+
+const ROOM_TO_ACTIVE_THREAD = 'roomToActiveThread';
+
+const getStoreKey = (userId: string): string => `${ROOM_TO_ACTIVE_THREAD}${userId}`;
+
+type RoomToActiveThread = Map;
+
+type RoomToActiveThreadAction =
+ | {
+ type: 'PUT';
+ roomId: string;
+ threadId: string;
+ }
+ | {
+ type: 'DELETE';
+ roomId: string;
+ };
+
+export type RoomToActiveThreadAtom = WritableAtom<
+ RoomToActiveThread,
+ [RoomToActiveThreadAction],
+ undefined
+>;
+
+export const makeRoomToActiveThreadAtom = (userId: string): RoomToActiveThreadAtom => {
+ const storeKey = getStoreKey(userId);
+
+ const baseRoomToActiveThread = atomWithLocalStorage(
+ storeKey,
+ (key) => {
+ const obj: Record = getLocalStorageItem(key, {});
+ return new Map(Object.entries(obj));
+ },
+ (key, value) => {
+ const obj: Record = Object.fromEntries(value);
+ setLocalStorageItem(key, obj);
+ }
+ );
+
+ const navToActivePathAtom = atom(
+ (get) => get(baseRoomToActiveThread),
+ (get, set, action) => {
+ if (action.type === 'DELETE') {
+ set(
+ baseRoomToActiveThread,
+ produce(get(baseRoomToActiveThread), (draft) => {
+ draft.delete(action.roomId);
+ })
+ );
+ return;
+ }
+ if (action.type === 'PUT') {
+ set(
+ baseRoomToActiveThread,
+ produce(get(baseRoomToActiveThread), (draft) => {
+ draft.set(action.roomId, action.threadId);
+ })
+ );
+ }
+ }
+ );
+
+ return navToActivePathAtom;
+};
+
+export const clearRoomToActiveThreadStore = (userId: string) => {
+ localStorage.removeItem(getStoreKey(userId));
+};
diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts
index 6a894350..31ee6ccb 100644
--- a/src/app/state/settings.ts
+++ b/src/app/state/settings.ts
@@ -1,13 +1,7 @@
import { atom } from 'jotai';
const STORAGE_KEY = 'settings';
-export type DateFormat =
- | 'D MMM YYYY'
- | 'DD/MM/YYYY'
- | 'MM/DD/YYYY'
- | 'YYYY/MM/DD'
- | 'YYYY-MM-DD'
- | '';
+export type DateFormat = 'D MMM YYYY' | 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD' | '';
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
export enum MessageLayout {
Modern = 0,
@@ -50,8 +44,8 @@ export interface Settings {
}
const defaultSettings: Settings = {
- themeId: 'gamecult-theme',
- useSystemTheme: false,
+ themeId: undefined,
+ useSystemTheme: true,
lightThemeId: undefined,
darkThemeId: undefined,
monochromeMode: false,
@@ -61,10 +55,10 @@ const defaultSettings: Settings = {
pageZoom: 100,
hideActivity: false,
- isPeopleDrawer: false,
+ isPeopleDrawer: true,
memberSortFilterIndex: 0,
enterForNewline: false,
- messageLayout: 2,
+ messageLayout: 0,
messageSpacing: '400',
hideMembershipEvents: false,
hideNickAvatarEvents: true,
diff --git a/src/app/styles/Animations.css.ts b/src/app/styles/Animations.css.ts
deleted file mode 100644
index c6bdc565..00000000
--- a/src/app/styles/Animations.css.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { keyframes, style } from '@vanilla-extract/css';
-import { color, toRem } from 'folds';
-
-const wobble = keyframes({
- '0%': {
- transform: 'translateX(0) rotateZ(0deg)',
- },
- '20%': {
- transform: `translateX(-${toRem(4)}) rotateZ(-4deg)`,
- },
- '40%': {
- transform: `translateX(${toRem(4)}) rotateZ(4deg)`,
- },
- '60%': {
- transform: `translateX(-${toRem(3)}) rotateZ(-3deg)`,
- },
- '80%': {
- transform: `translateX(${toRem(3)}) rotateZ(3deg)`,
- },
- '100%': {
- transform: 'translateX(0) rotateZ(0deg)',
- },
-});
-
-const glowPulse = keyframes({
- '0%': {
- boxShadow: `0 0 0 ${toRem(0)} ${color.Success.ContainerActive}`,
- },
- '100%': {
- boxShadow: `0 0 0 ${toRem(8)} ${color.Success.ContainerActive}`,
- },
-});
-
-export const WobbleAnimation = style({
- animation: `${wobble} 2000ms ease-in-out`,
- animationIterationCount: 'infinite',
-});
-
-export const GlowAnimation = style({
- animation: `${glowPulse} 2000ms ease-out`,
- animationIterationCount: 'infinite',
-});
-
-export const CallAvatarAnimation = style({
- animation: `${wobble} 2000ms ease-in-out, ${glowPulse} 2000ms ease-out`,
- animationIterationCount: 'infinite',
-});
diff --git a/src/app/styles/CustomHtml.css.ts b/src/app/styles/CustomHtml.css.ts
index 51f841b7..ba7b9214 100644
--- a/src/app/styles/CustomHtml.css.ts
+++ b/src/app/styles/CustomHtml.css.ts
@@ -120,23 +120,12 @@ export const CodeBlockBottomShadow = style({
background: `linear-gradient(to top, #00000022, #00000000)`,
});
-const BaseList = style({});
export const List = style([
- BaseList,
DefaultReset,
MarginSpaced,
{
padding: `0 ${config.space.S100}`,
paddingLeft: config.space.S600,
- selectors: {
- '& &': {
- marginTop: config.space.S200,
- marginBottom: config.space.S200,
- },
- 'li:last-child &': {
- marginBottom: 0,
- },
- },
},
]);
diff --git a/src/app/utils/common.ts b/src/app/utils/common.ts
index 6bda2802..678f1b6e 100644
--- a/src/app/utils/common.ts
+++ b/src/app/utils/common.ts
@@ -87,21 +87,13 @@ export const scaleYDimension = (x: number, scaledX: number, y: number): number =
};
export const parseGeoUri = (location: string) => {
- try {
- const [, data] = location.split(':');
- const [cords] = data.split(';');
- const [latitude, longitude] = cords.split(',');
-
- if (typeof latitude === 'string' && typeof longitude === 'string') {
- return {
- latitude,
- longitude,
- };
- }
- return undefined;
- } catch {
- return undefined;
- }
+ const [, data] = location.split(':');
+ const [cords] = data.split(';');
+ const [latitude, longitude] = cords.split(',');
+ return {
+ latitude,
+ longitude,
+ };
};
const START_SLASHES_REG = /^\/+/g;
diff --git a/src/app/utils/dom.ts b/src/app/utils/dom.ts
index ba6fb44c..80db3ae7 100644
--- a/src/app/utils/dom.ts
+++ b/src/app/utils/dom.ts
@@ -233,15 +233,7 @@ export const notificationPermission = (permission: NotificationPermission) => {
if ('Notification' in window) {
return window.Notification.permission === permission;
}
- try {
- // https://stackoverflow.com/questions/29774836/failed-to-construct-notification-illegal-constructor
- // https://issues.chromium.org/issues/40415865
- // eslint-disable-next-line no-new
- new Notification('');
- } catch {
- return false;
- }
- return true;
+ return false;
};
export const getMouseEventCords = (event: MouseEvent) => ({
diff --git a/src/app/utils/mDirect.test.ts b/src/app/utils/mDirect.test.ts
deleted file mode 100644
index 1deaf37e..00000000
--- a/src/app/utils/mDirect.test.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import { addRoomIdToMDirect, removeRoomIdFromMDirect } from './matrix';
-
-type Content = Record;
-
-/** A client whose account-data write takes a while and lands only when it completes. */
-const fakeClient = (initial?: Content) => {
- let direct: Content | undefined = initial;
- const writes: Content[] = [];
- const mx = {
- getAccountData: () => (direct ? { getContent: () => direct } : undefined),
- setAccountData: async (_type: string, content: Content) => {
- await new Promise((r) => {
- setTimeout(r, 5);
- });
- writes.push(structuredClone(content));
- direct = structuredClone(content);
- },
- };
- return { mx: mx as never, writes, current: () => direct };
-};
-
-describe('m.direct edits', () => {
- it('moves a room between users and removes it, one write at a time', async () => {
- const c = fakeClient({ '@a:test': ['!r:test'] });
- await Promise.all([
- addRoomIdToMDirect(c.mx, '!r:test', '@b:test'),
- removeRoomIdFromMDirect(c.mx, '!gone:test'),
- ]);
- expect(c.current()).toEqual({ '@a:test': [], '@b:test': ['!r:test'] });
- await removeRoomIdFromMDirect(c.mx, '!r:test');
- expect(c.current()).toEqual({ '@a:test': [], '@b:test': [] });
- });
-
- it('serializes overlapping edits so each builds on the content the last one wrote', async () => {
- const c = fakeClient();
- await Promise.all([
- addRoomIdToMDirect(c.mx, '!dm2:test', '@friend:test'),
- addRoomIdToMDirect(c.mx, '!dm3:test', '@friend:test'),
- addRoomIdToMDirect(c.mx, '!dm4:test', '@other:test'),
- ]);
- expect(c.current()).toEqual({
- '@friend:test': ['!dm2:test', '!dm3:test'],
- '@other:test': ['!dm4:test'],
- });
- expect(c.writes).toHaveLength(3);
- });
-
- it('keeps going after a failed write', async () => {
- const c = fakeClient();
- const raw = c.mx as unknown as { setAccountData: (t: string, x: Content) => Promise };
- const original = raw.setAccountData;
- raw.setAccountData = async () => {
- raw.setAccountData = original;
- throw new Error('offline');
- };
- await expect(addRoomIdToMDirect(c.mx, '!a:test', '@f:test')).rejects.toThrow('offline');
- await addRoomIdToMDirect(c.mx, '!a:test', '@f:test');
- expect(c.current()).toEqual({ '@f:test': ['!a:test'] });
- });
-});
diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts
index 2859dbc3..4c86c4e2 100644
--- a/src/app/utils/matrix.ts
+++ b/src/app/utils/matrix.ts
@@ -225,59 +225,57 @@ export const guessDmRoomUserId = (room: Room, myUserId: string): string => {
return member1?.userId ?? myUserId;
};
-type MDirectContent = Record;
-
-const mDirectWrites = new WeakMap>();
-
-/**
- * The only writer of m.direct. m.direct is one account-data event that every
- * edit replaces whole, so edits run one at a time, each reading the content the
- * previous one wrote. `edit` returns the new content, or undefined for no write.
- */
-export const updateMDirect = (
- mx: MatrixClient,
- edit: (current: MDirectContent) => MDirectContent | undefined
-): Promise => {
- const run = (mDirectWrites.get(mx) ?? Promise.resolve()).then(async () => {
- const event = mx.getAccountData(AccountDataEvent.Direct as any);
- const next = edit(event ? structuredClone(event.getContent()) : {});
- if (next) await mx.setAccountData(AccountDataEvent.Direct as any, next as any);
- });
- mDirectWrites.set(
- mx,
- run.catch(() => undefined)
- );
- return run;
-};
-
-/** A room can be a DM for one person only: it leaves every other user's list. */
-export const addRoomIdToMDirect = (
+export const addRoomIdToMDirect = async (
mx: MatrixClient,
roomId: string,
userId: string
-): Promise =>
- updateMDirect(mx, (current) => {
- const next = structuredClone(current);
- Object.keys(next).forEach((targetUserId) => {
- if (targetUserId !== userId) {
- next[targetUserId] = next[targetUserId].filter((id) => id !== roomId);
+): Promise => {
+ const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as any);
+ let userIdToRoomIds: Record = {};
+
+ if (typeof mDirectsEvent !== 'undefined')
+ userIdToRoomIds = structuredClone(mDirectsEvent.getContent());
+
+ // remove it from the lists of any others users
+ // (it can only be a DM room for one person)
+ Object.keys(userIdToRoomIds).forEach((targetUserId) => {
+ const roomIds = userIdToRoomIds[targetUserId];
+
+ if (targetUserId !== userId) {
+ const indexOfRoomId = roomIds.indexOf(roomId);
+ if (indexOfRoomId > -1) {
+ roomIds.splice(indexOfRoomId, 1);
}
- });
- const roomIds = next[userId] ?? [];
- if (!roomIds.includes(roomId)) roomIds.push(roomId);
- next[userId] = roomIds;
- return next;
+ }
});
-export const removeRoomIdFromMDirect = (mx: MatrixClient, roomId: string): Promise =>
- updateMDirect(mx, (current) => {
- const next: MDirectContent = {};
- Object.keys(current).forEach((userId) => {
- next[userId] = current[userId].filter((id) => id !== roomId);
- });
- return next;
+ const roomIds = userIdToRoomIds[userId] || [];
+ if (roomIds.indexOf(roomId) === -1) {
+ roomIds.push(roomId);
+ }
+ userIdToRoomIds[userId] = roomIds;
+
+ await mx.setAccountData(AccountDataEvent.Direct as any, userIdToRoomIds as any);
+};
+
+export const removeRoomIdFromMDirect = async (mx: MatrixClient, roomId: string): Promise => {
+ const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as any);
+ let userIdToRoomIds: Record = {};
+
+ if (typeof mDirectsEvent !== 'undefined')
+ userIdToRoomIds = structuredClone(mDirectsEvent.getContent());
+
+ Object.keys(userIdToRoomIds).forEach((targetUserId) => {
+ const roomIds = userIdToRoomIds[targetUserId];
+ const indexOfRoomId = roomIds.indexOf(roomId);
+ if (indexOfRoomId > -1) {
+ roomIds.splice(indexOfRoomId, 1);
+ }
});
+ await mx.setAccountData(AccountDataEvent.Direct as any, userIdToRoomIds as any);
+};
+
export const mxcUrlToHttp = (
mx: MatrixClient,
mxcUrl: string,
diff --git a/src/app/utils/mimeTypes.ts b/src/app/utils/mimeTypes.ts
index e9d1e35b..98c5aee9 100644
--- a/src/app/utils/mimeTypes.ts
+++ b/src/app/utils/mimeTypes.ts
@@ -31,7 +31,6 @@ export const APPLICATION_MIME_TYPES = [
'application/javascript',
'application/xhtml+xml',
'application/xml',
- 'application/ogg',
];
export const TEXT_MIME_TYPE = [
@@ -116,10 +115,6 @@ export const getBlobSafeMimeType = (mimeType: string) => {
if (type === 'video/quicktime') {
return 'video/mp4';
}
- // Fixes missing playback for Ogg audio
- if (type === 'application/ogg') {
- return 'audio/ogg';
- }
return type;
};
diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts
index 05dfeb29..f7b8f017 100644
--- a/src/app/utils/room.ts
+++ b/src/app/utils/room.ts
@@ -8,6 +8,7 @@ import {
IPowerLevelsContent,
IPushRule,
IPushRules,
+ IThreadBundledRelationship,
JoinRule,
MatrixClient,
MatrixEvent,
@@ -16,6 +17,7 @@ import {
RelationType,
Room,
RoomMember,
+ THREAD_RELATION_TYPE,
} from 'matrix-js-sdk';
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
import { AccountDataEvent } from '../../types/matrix/accountData';
@@ -160,9 +162,7 @@ export const getOrphanParents = (roomToParents: RoomToParents, roomId: string):
};
export const isMutedRule = (rule: IPushRule) =>
- // Check for empty actions (new spec) or dont_notify (deprecated)
- (rule.actions.length === 0 || rule.actions[0] === 'dont_notify') &&
- rule.conditions?.[0]?.kind === 'event_match';
+ rule.actions[0] === 'dont_notify' && rule.conditions?.[0]?.kind === 'event_match';
export const findMutedRule = (overrideRules: IPushRule[], roomId: string) =>
overrideRules.find((rule) => rule.rule_id === roomId && isMutedRule(rule));
@@ -203,6 +203,7 @@ export const isNotificationEvent = (mEvent: MatrixEvent) => {
if (mEvent.isRedacted()) return false;
if (mEvent.getRelation()?.rel_type === 'm.replace') return false;
+ if (mEvent.getRelation()?.rel_type === 'm.thread') return false;
return true;
};
@@ -258,44 +259,24 @@ export const getUnreadInfos = (mx: MatrixClient): UnreadInfo[] => {
return unreadInfos;
};
-export const getRoomIconSrc = (
+export const joinRuleToIconSrc = (
icons: Record,
- roomType?: string,
- joinRule?: JoinRule
-): IconSrc => {
- if (roomType === RoomType.Space) {
- if (joinRule === JoinRule.Public) return icons.SpaceGlobe;
- if (
- joinRule === JoinRule.Invite ||
- joinRule === JoinRule.Knock ||
- joinRule === JoinRule.Private
- ) {
- return icons.SpaceLock;
- }
- return icons.Space;
+ joinRule: JoinRule,
+ space: boolean
+): IconSrc | undefined => {
+ if (joinRule === JoinRule.Restricted) {
+ return space ? icons.Space : icons.Hash;
}
-
- if (roomType === RoomType.Call) {
- if (joinRule === JoinRule.Public) return icons.VolumeHighGlobe;
- if (
- joinRule === JoinRule.Invite ||
- joinRule === JoinRule.Knock ||
- joinRule === JoinRule.Private
- ) {
- return icons.VolumeHighLock;
- }
- return icons.VolumeHigh;
+ if (joinRule === JoinRule.Knock) {
+ return space ? icons.SpaceLock : icons.HashLock;
}
-
- if (joinRule === JoinRule.Public) return icons.HashGlobe;
- if (
- joinRule === JoinRule.Invite ||
- joinRule === JoinRule.Knock ||
- joinRule === JoinRule.Private
- ) {
- return icons.HashLock;
+ if (joinRule === JoinRule.Invite) {
+ return space ? icons.SpaceLock : icons.HashLock;
}
- return icons.Hash;
+ if (joinRule === JoinRule.Public) {
+ return space ? icons.SpaceGlobe : icons.HashGlobe;
+ }
+ return undefined;
};
export const getRoomAvatarUrl = (
@@ -573,3 +554,13 @@ export const guessPerfectParent = (
return perfectParent;
};
+
+export const getEventThreadDetail = (
+ mEvent: MatrixEvent
+): IThreadBundledRelationship | undefined => {
+ const details = mEvent.getServerAggregatedRelation(
+ THREAD_RELATION_TYPE.name
+ );
+
+ return details;
+};
diff --git a/src/app/utils/rtc.ts b/src/app/utils/rtc.ts
deleted file mode 100644
index 6a40504c..00000000
--- a/src/app/utils/rtc.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const webRTCSupported = () =>
- ['RTCPeerConnection', 'webkitRTCPeerConnection', 'mozRTCPeerConnection', 'RTCIceGatherer'].some(
- (item) => item in window
- );
diff --git a/src/app/utils/sanitize.ts b/src/app/utils/sanitize.ts
index c199f69a..985c47b1 100644
--- a/src/app/utils/sanitize.ts
+++ b/src/app/utils/sanitize.ts
@@ -71,7 +71,7 @@ const permittedTagToAttributes = {
ul: ['data-md'],
a: ['name', 'target', 'href', 'rel', 'data-md'],
img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'],
- code: ['class', 'data-md', 'data-label'],
+ code: ['class', 'data-md'],
strong: ['data-md'],
i: ['data-md'],
em: ['data-md'],
@@ -100,7 +100,7 @@ const transformATag: Transformer = (tagName, attribs) => ({
tagName,
attribs: {
...attribs,
- rel: 'noreferrer noopener',
+ rel: 'noopener',
target: '_blank',
},
});
@@ -112,7 +112,7 @@ const transformImgTag: Transformer = (tagName, attribs) => {
tagName: 'a',
attribs: {
href: src,
- rel: 'noreferrer noopener',
+ rel: 'noopener',
target: '_blank',
},
text: attribs.alt || src,
diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts
index 498d4f75..f26b4f8d 100644
--- a/src/client/initMatrix.ts
+++ b/src/client/initMatrix.ts
@@ -2,7 +2,7 @@ import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from
import { cryptoCallbacks } from './secretStorageKeys';
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
-import { pushSessionToSW } from '../sw-session';
+import { clearRoomToActiveThreadStore } from '../app/state/roomToActiveThread';
type Session = {
baseUrl: string;
@@ -43,18 +43,19 @@ export const initClient = async (session: Session): Promise => {
export const startClient = async (mx: MatrixClient) => {
await mx.startClient({
lazyLoadMembers: true,
+ threadSupport: true,
});
};
export const clearCacheAndReload = async (mx: MatrixClient) => {
mx.stopClient();
clearNavToActivePathStore(mx.getSafeUserId());
+ clearRoomToActiveThreadStore(mx.getSafeUserId());
await mx.store.deleteAllData();
window.location.reload();
};
export const logoutClient = async (mx: MatrixClient) => {
- pushSessionToSW();
mx.stopClient();
try {
await mx.logout();
diff --git a/src/colors.css.ts b/src/colors.css.ts
index 9c03c2cb..268cbf78 100644
--- a/src/colors.css.ts
+++ b/src/colors.css.ts
@@ -236,95 +236,3 @@ export const butterTheme = createTheme(color, {
OnContainer: '#F2EED3',
},
});
-
-// GameCult identity (gamecult-site quartz.config.ts): single dark theme on #07111a,
-// orange #ff8a2a accent, sky #59b7ff links. The brand defines no status colours, so
-// Success/Warning/Critical are invented here and kept distinct from the orange accent.
-export const gamecultTheme = createTheme(color, {
- Background: {
- Container: '#07111a',
- ContainerHover: '#0d1a26',
- ContainerActive: '#122130',
- ContainerLine: '#16212c',
- OnContainer: '#b7c7d9',
- },
- Surface: {
- Container: '#09141f',
- ContainerHover: '#0f1c29',
- ContainerActive: '#152433',
- ContainerLine: '#16212c',
- OnContainer: '#b7c7d9',
- },
- SurfaceVariant: {
- Container: '#0d1a26',
- ContainerHover: '#122130',
- ContainerActive: '#16273a',
- ContainerLine: '#1b2a38',
- OnContainer: '#b7c7d9',
- },
- Primary: {
- Main: '#ff8a2a',
- MainHover: '#ff9640',
- MainActive: '#ffa155',
- MainLine: '#ffab66',
- OnMain: '#2a1300',
- Container: '#2b1a0e',
- ContainerHover: '#35200f',
- ContainerActive: '#402611',
- ContainerLine: '#4d2d13',
- OnContainer: '#ffd3ab',
- },
- Secondary: {
- Main: '#eef5ff',
- MainHover: '#dbe7f6',
- MainActive: '#cddced',
- MainLine: '#bfd0e4',
- OnMain: '#07111a',
- Container: '#16212c',
- ContainerHover: '#1c2a38',
- ContainerActive: '#233342',
- ContainerLine: '#2a3b4c',
- OnContainer: '#eef5ff',
- },
- Success: {
- Main: '#5fd0a4',
- MainHover: '#72d8b0',
- MainActive: '#84deba',
- MainLine: '#93e3c2',
- OnMain: '#042014',
- Container: '#0c2a21',
- ContainerHover: '#10342a',
- ContainerActive: '#143e32',
- ContainerLine: '#19493a',
- OnContainer: '#c4f1de',
- },
- Warning: {
- Main: '#f2d04b',
- MainHover: '#f5d963',
- MainActive: '#f7e078',
- MainLine: '#f9e68b',
- OnMain: '#2a2100',
- Container: '#2a2410',
- ContainerHover: '#352d13',
- ContainerActive: '#403616',
- ContainerLine: '#4d411a',
- OnContainer: '#f8ebb0',
- },
- Critical: {
- Main: '#ff7a90',
- MainHover: '#ff8fa1',
- MainActive: '#ffa0af',
- MainLine: '#ffb0bc',
- OnMain: '#3a0710',
- Container: '#2e1118',
- ContainerHover: '#391520',
- ContainerActive: '#441a27',
- ContainerLine: '#51202e',
- OnContainer: '#ffd0d8',
- },
- Other: {
- FocusRing: 'rgba(89, 183, 255, 0.55)',
- Shadow: 'rgba(0, 0, 0, 0.6)',
- Overlay: 'rgba(3, 7, 13, 0.8)',
- },
-});
diff --git a/src/gamecult.css.ts b/src/gamecult.css.ts
deleted file mode 100644
index 4aaf3e76..00000000
--- a/src/gamecult.css.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { globalStyle } from '@vanilla-extract/css';
-import { config } from 'folds';
-
-// Softer, rounder shape language and an airier line height. Scoped to the GameCult
-// theme class so the stock themes keep folds' defaults.
-const scope = 'body.gamecult-theme';
-
-globalStyle(scope, {
- vars: {
- [config.radii.R300]: '10px',
- [config.radii.R400]: '16px',
- [config.radii.R500]: '22px',
- [config.lineHeight.T300]: '1.6',
- [config.lineHeight.T400]: '1.7',
- [config.lineHeight.T500]: '1.7',
- [config.lineHeight.B400]: '1.6',
- [config.lineHeight.B500]: '1.6',
- [config.lineHeight.D400]: '1.7',
- },
-});
diff --git a/src/index.css b/src/index.css
index f751b94d..ca28536d 100644
--- a/src/index.css
+++ b/src/index.css
@@ -129,73 +129,3 @@ textarea {
audio:not([controls]) {
display: none !important;
}
-
-/* Fix Firefox rendering lists that have empty items with those items collapsed in on eachother */
-li p::before {
- content: '';
- display: inline-block;
-}
-
-/* GameCult identity: Ubuntu 300 prose, Montserrat thin titles, IBM Plex Mono labels. */
-.gamecult-theme {
- --tc-link: #59b7ff;
-
- --mx-uc-1: hsl(208, 100%, 72%);
- --mx-uc-2: hsl(280, 100%, 78%);
- --mx-uc-3: hsl(163, 70%, 65%);
- --mx-uc-4: hsl(343, 100%, 76%);
- --mx-uc-5: hsl(28, 100%, 66%);
- --mx-uc-6: hsl(181, 70%, 62%);
- --mx-uc-7: hsl(243, 100%, 80%);
- --mx-uc-8: hsl(94, 70%, 70%);
-
- --font-secondary: 'Ubuntu', var(--font-emoji), sans-serif;
- --font-title: 'Montserrat', 'Ubuntu', sans-serif;
- --font-mono: 'IBM Plex Mono', monospace;
- --gamecult-rule: rgba(148, 163, 184, 0.22);
- --gamecult-wash: 0.5;
-
- background-color: #07111a;
- background-image: radial-gradient(
- circle at 78% 14%,
- rgb(255 138 42 / calc(0.18 * var(--gamecult-wash))),
- transparent 18%
- ),
- radial-gradient(
- circle at 18% 10%,
- rgb(109 96 255 / calc(0.16 * var(--gamecult-wash))),
- transparent 24%
- ),
- radial-gradient(
- circle at 52% 0%,
- rgb(89 183 255 / calc(0.14 * var(--gamecult-wash))),
- transparent 28%
- ),
- linear-gradient(180deg, #03070d 0%, #07111a 44%, #09141f 100%);
- background-attachment: fixed;
- color-scheme: dark;
- font-weight: 300;
-}
-
-.gamecult-theme b,
-.gamecult-theme strong {
- font-weight: 500;
-}
-
-.gamecult-theme h1,
-.gamecult-theme h2,
-.gamecult-theme h3 {
- font-family: var(--font-title);
- font-weight: 300;
-}
-
-.gamecult-theme code,
-.gamecult-theme pre {
- font-family: var(--font-mono);
-}
-
-/* Quiet secondary metadata: timestamps, edited marks and the like sit back. */
-.gamecult-theme time {
- opacity: 0.55;
- font-size: 0.78em;
-}
diff --git a/src/index.tsx b/src/index.tsx
index 672ac5c0..ddfd30b4 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -3,13 +3,6 @@ import React from 'react';
import { createRoot } from 'react-dom/client';
import { enableMapSet } from 'immer';
import '@fontsource/inter/variable.css';
-import '@fontsource/ubuntu/300.css';
-import '@fontsource/ubuntu/400.css';
-import '@fontsource/ubuntu/500.css';
-import '@fontsource/ubuntu/700.css';
-import '@fontsource/montserrat/200.css';
-import '@fontsource/montserrat/300.css';
-import '@fontsource/ibm-plex-mono/400.css';
import 'folds/dist/style.css';
import { configClass, varsClass } from 'folds';
@@ -22,8 +15,6 @@ import App from './app/pages/App';
// import i18n (needs to be bundled ;))
import './app/i18n';
-import { pushSessionToSW } from './sw-session';
-import { getFallbackSession } from './app/state/sessions';
document.body.classList.add(configClass, varsClass);
@@ -34,19 +25,15 @@ if ('serviceWorker' in navigator) {
? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
: `/dev-sw.js?dev-sw`;
- const sendSessionToSW = () => {
- const session = getFallbackSession();
- pushSessionToSW(session?.baseUrl, session?.accessToken);
- };
-
- navigator.serviceWorker.register(swUrl).then(sendSessionToSW);
- navigator.serviceWorker.ready.then(sendSessionToSW);
-
- navigator.serviceWorker.addEventListener('message', (ev) => {
- const { type } = ev.data ?? {};
-
- if (type === 'requestSession') {
- sendSessionToSW();
+ navigator.serviceWorker.register(swUrl);
+ navigator.serviceWorker.addEventListener('message', (event) => {
+ if (event.data?.type === 'token' && event.data?.responseKey) {
+ // Get the token for SW.
+ const token = localStorage.getItem('cinny_access_token') ?? undefined;
+ event.source!.postMessage({
+ responseKey: event.data.responseKey,
+ token,
+ });
}
});
}
diff --git a/src/sw-session.ts b/src/sw-session.ts
deleted file mode 100644
index 4b2ec055..00000000
--- a/src/sw-session.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export function pushSessionToSW(baseUrl?: string, accessToken?: string) {
- if (!('serviceWorker' in navigator)) return;
- if (!navigator.serviceWorker.controller) return;
-
- navigator.serviceWorker.controller.postMessage({
- type: 'setSession',
- accessToken,
- baseUrl,
- });
-}
diff --git a/src/sw.ts b/src/sw.ts
index 69293b1d..2179dfcb 100644
--- a/src/sw.ts
+++ b/src/sw.ts
@@ -3,126 +3,22 @@
export type {};
declare const self: ServiceWorkerGlobalScope;
-type SessionInfo = {
- accessToken: string;
- baseUrl: string;
-};
-
-/**
- * Store session per client (tab)
- */
-const sessions = new Map();
-
-const clientToResolve = new Map void>();
-const clientToSessionPromise = new Map>();
-
-async function cleanupDeadClients() {
- const activeClients = await self.clients.matchAll();
- const activeIds = new Set(activeClients.map((c) => c.id));
-
- Array.from(sessions.keys()).forEach((id) => {
- if (!activeIds.has(id)) {
- sessions.delete(id);
- clientToResolve.delete(id);
- clientToSessionPromise.delete(id);
- }
+async function askForAccessToken(client: Client): Promise {
+ return new Promise((resolve) => {
+ const responseKey = Math.random().toString(36);
+ const listener = (event: ExtendableMessageEvent) => {
+ if (event.data.responseKey !== responseKey) return;
+ resolve(event.data.token);
+ self.removeEventListener('message', listener);
+ };
+ self.addEventListener('message', listener);
+ client.postMessage({ responseKey, type: 'token' });
});
}
-function setSession(clientId: string, accessToken: any, baseUrl: any) {
- if (typeof accessToken === 'string' && typeof baseUrl === 'string') {
- sessions.set(clientId, { accessToken, baseUrl });
- } else {
- // Logout or invalid session
- sessions.delete(clientId);
- }
+function fetchConfig(token?: string): RequestInit | undefined {
+ if (!token) return undefined;
- const resolveSession = clientToResolve.get(clientId);
- if (resolveSession) {
- resolveSession(sessions.get(clientId));
- clientToResolve.delete(clientId);
- clientToSessionPromise.delete(clientId);
- }
-}
-
-function requestSession(client: Client): Promise {
- const promise =
- clientToSessionPromise.get(client.id) ??
- new Promise((resolve) => {
- clientToResolve.set(client.id, resolve);
- client.postMessage({ type: 'requestSession' });
- });
-
- if (!clientToSessionPromise.has(client.id)) {
- clientToSessionPromise.set(client.id, promise);
- }
-
- return promise;
-}
-
-async function requestSessionWithTimeout(
- clientId: string,
- timeoutMs = 3000
-): Promise {
- const client = await self.clients.get(clientId);
- if (!client) return undefined;
-
- const sessionPromise = requestSession(client);
-
- const timeout = new Promise((resolve) => {
- setTimeout(() => resolve(undefined), timeoutMs);
- });
-
- return Promise.race([sessionPromise, timeout]);
-}
-
-self.addEventListener('install', () => {
- self.skipWaiting();
-});
-
-self.addEventListener('activate', (event: ExtendableEvent) => {
- event.waitUntil(
- (async () => {
- await self.clients.claim();
- await cleanupDeadClients();
- })()
- );
-});
-
-/**
- * Receive session updates from clients
- */
-self.addEventListener('message', (event: ExtendableMessageEvent) => {
- const client = event.source as Client | null;
- if (!client) return;
-
- const { type, accessToken, baseUrl } = event.data || {};
-
- if (type === 'setSession') {
- setSession(client.id, accessToken, baseUrl);
- cleanupDeadClients();
- }
-});
-
-const MEDIA_PATHS = ['/_matrix/client/v1/media/download', '/_matrix/client/v1/media/thumbnail'];
-
-function mediaPath(url: string): boolean {
- try {
- const { pathname } = new URL(url);
- return MEDIA_PATHS.some((p) => pathname.startsWith(p));
- } catch {
- return false;
- }
-}
-
-function validMediaRequest(url: string, baseUrl: string): boolean {
- return MEDIA_PATHS.some((p) => {
- const validUrl = new URL(p, baseUrl);
- return url.startsWith(validUrl.href);
- });
-}
-
-function fetchConfig(token: string): RequestInit {
return {
headers: {
Authorization: `Bearer ${token}`,
@@ -131,28 +27,26 @@ function fetchConfig(token: string): RequestInit {
};
}
+self.addEventListener('activate', (event: ExtendableEvent) => {
+ event.waitUntil(clients.claim());
+});
+
self.addEventListener('fetch', (event: FetchEvent) => {
const { url, method } = event.request;
-
- if (method !== 'GET' || !mediaPath(url)) return;
-
- const { clientId } = event;
- if (!clientId) return;
-
- const session = sessions.get(clientId);
- if (session) {
- if (validMediaRequest(url, session.baseUrl)) {
- event.respondWith(fetch(url, fetchConfig(session.accessToken)));
- }
+ if (method !== 'GET') return;
+ if (
+ !url.includes('/_matrix/client/v1/media/download') &&
+ !url.includes('/_matrix/client/v1/media/thumbnail')
+ ) {
return;
}
-
event.respondWith(
- requestSessionWithTimeout(clientId).then((s) => {
- if (s && validMediaRequest(url, s.baseUrl)) {
- return fetch(url, fetchConfig(s.accessToken));
- }
- return fetch(event.request);
- })
+ (async (): Promise => {
+ const client = await self.clients.get(event.clientId);
+ let token: string | undefined;
+ if (client) token = await askForAccessToken(client);
+
+ return fetch(url, fetchConfig(token));
+ })()
);
});
diff --git a/src/types/matrix/accountData.ts b/src/types/matrix/accountData.ts
index 36c1e21b..98715996 100644
--- a/src/types/matrix/accountData.ts
+++ b/src/types/matrix/accountData.ts
@@ -3,6 +3,8 @@ export enum AccountDataEvent {
Direct = 'm.direct',
IgnoredUserList = 'm.ignored_user_list',
+ CinnySpaces = 'in.cinny.spaces',
+
ElementRecentEmoji = 'io.element.recent_emoji',
PoniesUserEmotes = 'im.ponies.user_emotes',
diff --git a/src/types/matrix/room.ts b/src/types/matrix/room.ts
index c73c0f0b..b866fd77 100644
--- a/src/types/matrix/room.ts
+++ b/src/types/matrix/room.ts
@@ -32,8 +32,6 @@ export enum StateEvent {
RoomGuestAccess = 'm.room.guest_access',
RoomServerAcl = 'm.room.server_acl',
RoomTombstone = 'm.room.tombstone',
- GroupCallPrefix = 'org.matrix.msc3401.call',
- GroupCallMemberPrefix = 'org.matrix.msc3401.call.member',
SpaceChild = 'm.space.child',
SpaceParent = 'm.space.parent',
@@ -52,7 +50,6 @@ export enum MessageEvent {
export enum RoomType {
Space = 'm.space',
- Call = 'org.matrix.msc3417.call',
}
export type MSpaceChildContent = {
diff --git a/vite.config.js b/vite.config.js
index a5af94d8..dfa02fc4 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -13,10 +13,6 @@ import buildConfig from './build.config';
const copyFiles = {
targets: [
- {
- src: 'node_modules/@element-hq/element-call-embedded/dist/*',
- dest: 'public/element-call',
- },
{
src: 'node_modules/pdfjs-dist/build/pdf.worker.min.mjs',
dest: '',
@@ -51,10 +47,7 @@ function serverMatrixSdkCryptoWasm(wasmFilePath) {
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === wasmFilePath) {
- const resolvedPath = path.join(
- path.resolve(),
- '/node_modules/@matrix-org/matrix-sdk-crypto-wasm/pkg/matrix_sdk_crypto_wasm_bg.wasm'
- );
+ const resolvedPath = path.join(path.resolve(), "/node_modules/@matrix-org/matrix-sdk-crypto-wasm/pkg/matrix_sdk_crypto_wasm_bg.wasm");
if (fs.existsSync(resolvedPath)) {
res.setHeader('Content-Type', 'application/wasm');
@@ -109,8 +102,8 @@ export default defineConfig({
},
devOptions: {
enabled: true,
- type: 'module',
- },
+ type: 'module'
+ }
}),
],
optimizeDeps: {