forked from github/cinny
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a33e8db9a3 | ||
|
|
fb76e3ecb4 | ||
|
|
3d79293167 | ||
|
|
74745edcda | ||
|
|
0812131a97 | ||
|
|
1068bba5c7 | ||
|
|
1b5e58a3b4 | ||
|
|
acae043f31 | ||
|
|
b4299f8f37 | ||
|
|
b6adac6714 | ||
|
|
1c8f203164 | ||
|
|
0c30ece281 | ||
|
|
4e559e56d4 | ||
|
|
19f28b40ac | ||
|
|
bcaf43a540 | ||
|
|
9c7b635e7e | ||
|
|
65c87dff3a | ||
|
|
132a76df27 | ||
|
|
b0954eeddc | ||
|
|
8f1add6059 | ||
|
|
8a78c9699e | ||
|
|
0721b29a2c | ||
|
|
3d354909d6 | ||
|
|
7570a84dfd | ||
|
|
6a05ff5840 | ||
|
|
919fe8381b | ||
|
|
b76ad3caaf | ||
|
|
409d45857d | ||
|
|
0d1566977a | ||
|
|
0cbfbab5ad | ||
|
|
37e0c2aaac | ||
|
|
296249de32 | ||
|
|
4449e7c6e8 | ||
|
|
2eb5a9a616 | ||
|
|
d679e68501 | ||
|
|
bc6caddcc8 | ||
|
|
55e8306576 | ||
|
|
7953ec80e5 | ||
|
|
c6bb4915bc | ||
|
|
b050cd01f9 | ||
|
|
730670cf52 | ||
|
|
c5c8703699 | ||
|
|
2bd1570d6b | ||
|
|
68b6a09697 | ||
|
|
7b52c921d5 | ||
|
|
bb8b9ab6da | ||
|
|
971f312b46 | ||
|
|
e0d5c63dc5 | ||
|
|
85fcbd84fe | ||
|
|
221bc04754 | ||
|
|
6347640a35 | ||
|
|
f2d8ad0b6b | ||
|
|
739786d9ab | ||
|
|
f642809939 | ||
|
|
02106a99b9 | ||
|
|
df3a3ba789 | ||
|
|
cd80d4c9e8 | ||
|
|
dab44edef2 | ||
|
|
ed0ad61bc4 | ||
|
|
b2cb717178 | ||
|
|
7a9f6d2223 | ||
|
|
a9022184fc | ||
|
|
826b3c2997 | ||
|
|
2e6c5f7c04 | ||
|
|
2d6730de56 | ||
|
|
b6cc0e3077 | ||
|
|
91c8731940 | ||
|
|
1f03891b25 | ||
|
|
9ff15b8b03 | ||
|
|
170f5cd473 | ||
|
|
29ec172c8b | ||
|
|
0f220f50d6 | ||
|
|
d866c1b903 | ||
|
|
fbde1a2030 | ||
|
|
4ba7b9162d | ||
|
|
9d49418a1f | ||
|
|
3522751a15 | ||
|
|
074c555294 | ||
|
|
206a927f30 | ||
|
|
fd37dfe3f9 | ||
|
|
1ce6ca2b07 | ||
|
|
83e5125b37 | ||
|
|
ca82aa283a | ||
|
|
8ce33ee6ff | ||
|
|
073a9f5786 | ||
|
|
655c1c9aff | ||
|
|
17d4bceb42 | ||
|
|
0f61f2f328 |
127
.github/DISCUSSION_TEMPLATE/issue-triage.yml
vendored
Normal file
127
.github/DISCUSSION_TEMPLATE/issue-triage.yml
vendored
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
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: Environement
|
||||||
|
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
|
||||||
57
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
57
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,57 +0,0 @@
|
|||||||
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.
|
|
||||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 💬 Matrix Chat
|
- name: Features, Bug Reports, Questions
|
||||||
url: https://matrix.to/#/#cinny:matrix.org
|
url: https://github.com/cinnyapp/cinny/discussions/new/choose
|
||||||
about: Ask questions and talk to other Cinny users and the maintainers
|
about: Our preferred starting point if you have any questions or suggestions about features or behavior.
|
||||||
|
|||||||
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,33 +0,0 @@
|
|||||||
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.
|
|
||||||
9
.github/ISSUE_TEMPLATE/preapproved.md
vendored
Normal file
9
.github/ISSUE_TEMPLATE/preapproved.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
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.**
|
||||||
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,22 +0,0 @@
|
|||||||
<!-- Please read https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
|
|
||||||
|
|
||||||
### Description
|
|
||||||
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
- [ ] 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
|
|
||||||
3
.github/SECURITY.md
vendored
3
.github/SECURITY.md
vendored
@@ -1,3 +0,0 @@
|
|||||||
# Reporting a Vulnerability
|
|
||||||
|
|
||||||
**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**
|
|
||||||
20
.github/renovate.json
vendored
20
.github/renovate.json
vendored
@@ -1,10 +1,28 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": ["config:recommended", ":dependencyDashboardApproval"],
|
"extends": [
|
||||||
|
"config:recommended",
|
||||||
|
":dependencyDashboardApproval",
|
||||||
|
":semanticCommits",
|
||||||
|
"group:monorepos"
|
||||||
|
],
|
||||||
"labels": ["Dependencies"],
|
"labels": ["Dependencies"],
|
||||||
|
"rebaseWhen": "conflicted",
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["lockFileMaintenance"]
|
"matchUpdateTypes": ["lockFileMaintenance"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"groupName": "Slatejs",
|
||||||
|
"matchPackageNames": ["slate", "slate-dom", "slate-history", "slate-react"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"groupName": "Call",
|
||||||
|
"matchPackageNames": ["@element-hq/element-call-embedded", "matrix-widget-api"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"groupName": "Linkify",
|
||||||
|
"matchPackageNames": ["linkifyjs", "linkify-react"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lockFileMaintenance": {
|
"lockFileMaintenance": {
|
||||||
|
|||||||
12
.github/workflows/build-pull-request.yml
vendored
12
.github/workflows/build-pull-request.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
|||||||
PR_NUMBER: ${{github.event.number}}
|
PR_NUMBER: ${{github.event.number}}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: 20.12.2
|
node-version-file: ".node-version"
|
||||||
cache: 'npm'
|
package-manager-cache: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Build app
|
- name: Build app
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
run: npm run build
|
run: npm run build
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: preview
|
name: preview
|
||||||
path: dist
|
path: dist
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Save pr number
|
- name: Save pr number
|
||||||
run: echo ${PR_NUMBER} > ./pr.txt
|
run: echo ${PR_NUMBER} > ./pr.txt
|
||||||
- name: Upload pr number
|
- name: Upload pr number
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: pr
|
name: pr
|
||||||
path: ./pr.txt
|
path: ./pr.txt
|
||||||
|
|||||||
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- name: 'CLA Assistant'
|
- name: 'CLA Assistant'
|
||||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||||
# Beta Release
|
# Beta Release
|
||||||
uses: cla-assistant/github-action@v2.6.1
|
uses: cla-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||||
|
|||||||
15
.github/workflows/deploy-pull-request.yml
vendored
15
.github/workflows/deploy-pull-request.yml
vendored
@@ -1,4 +1,5 @@
|
|||||||
name: Deploy PR to Netlify
|
name: Deploy PR to Netlify
|
||||||
|
run-name: "Deploy PR to Netlify (${{ github.event.workflow_run.head_branch }})"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
@@ -15,7 +16,7 @@ jobs:
|
|||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Download pr number
|
- name: Download pr number
|
||||||
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
|
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||||
with:
|
with:
|
||||||
workflow: ${{ github.event.workflow.id }}
|
workflow: ${{ github.event.workflow.id }}
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
@@ -24,7 +25,7 @@ jobs:
|
|||||||
id: pr
|
id: pr
|
||||||
run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT
|
run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
|
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||||
with:
|
with:
|
||||||
workflow: ${{ github.event.workflow.id }}
|
workflow: ${{ github.event.workflow.id }}
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
@@ -32,7 +33,7 @@ jobs:
|
|||||||
path: dist
|
path: dist
|
||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
id: netlify
|
id: netlify
|
||||||
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
|
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
|
||||||
with:
|
with:
|
||||||
publish-dir: dist
|
publish-dir: dist
|
||||||
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
|
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
|
||||||
@@ -45,12 +46,12 @@ jobs:
|
|||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PR_CINNY }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PR_CINNY }}
|
||||||
timeout-minutes: 1
|
timeout-minutes: 1
|
||||||
- name: Comment preview on PR
|
- name: Comment preview on PR
|
||||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6
|
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b #v3.0.1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ steps.pr.outputs.id }}
|
pr-number: ${{ steps.pr.outputs.id }}
|
||||||
comment_tag: ${{ steps.pr.outputs.id }}
|
comment-tag: ${{ steps.pr.outputs.id }}
|
||||||
message: |
|
message: |
|
||||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||||
50
.github/workflows/docker-pr.yml
vendored
50
.github/workflows/docker-pr.yml
vendored
@@ -5,15 +5,59 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'Dockerfile'
|
- 'Dockerfile'
|
||||||
- '.github/workflows/docker-pr.yml'
|
- '.github/workflows/docker-pr.yml'
|
||||||
|
- '.github/workflows/prod-deploy.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-build:
|
docker-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Build Docker image
|
|
||||||
uses: docker/build-push-action@v6.18.0
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
|
|
||||||
|
- name: Login to Docker Hub #Do not update this action from a outside PR
|
||||||
|
if: github.event.pull_request.head.repo.fork == false
|
||||||
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker, GHCR
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
ajbura/cinny
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
|
- name: Build Docker image (no push)
|
||||||
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
platforms: linux/amd64
|
||||||
push: false
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
- name: Show Docker images
|
||||||
|
run: docker images
|
||||||
|
|||||||
4
.github/workflows/lockfile.yml
vendored
4
.github/workflows/lockfile.yml
vendored
@@ -14,9 +14,9 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: NPM Lockfile Changes
|
- name: NPM Lockfile Changes
|
||||||
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
|
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891 # v1.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# Optional inputs, can be deleted safely if you are happy with default values.
|
# Optional inputs, can be deleted safely if you are happy with default values.
|
||||||
|
|||||||
10
.github/workflows/netlify-dev.yml
vendored
10
.github/workflows/netlify-dev.yml
vendored
@@ -11,12 +11,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: 20.12.2
|
node-version-file: ".node-version"
|
||||||
cache: 'npm'
|
package-manager-cache: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Build app
|
- name: Build app
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
run: npm run build
|
run: npm run build
|
||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
|
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
|
||||||
with:
|
with:
|
||||||
publish-dir: dist
|
publish-dir: dist
|
||||||
deploy-message: 'Dev deploy ${{ github.sha }}'
|
deploy-message: 'Dev deploy ${{ github.sha }}'
|
||||||
|
|||||||
15
.github/workflows/pr-title.yml
vendored
Normal file
15
.github/workflows/pr-title.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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 }}
|
||||||
70
.github/workflows/prod-deploy.yml
vendored
70
.github/workflows/prod-deploy.yml
vendored
@@ -1,32 +1,48 @@
|
|||||||
name: Production deploy
|
name: Production deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
workflow_dispatch:
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy-and-tarball:
|
deploy-and-tarball:
|
||||||
name: Netlify deploy and tarball
|
name: Netlify deploy and tarball
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.vars.outputs.tag }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup node
|
|
||||||
uses: actions/setup-node@v4.4.0
|
|
||||||
with:
|
with:
|
||||||
node-version: 20.12.2
|
fetch-depth: 0
|
||||||
cache: 'npm'
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
|
with:
|
||||||
|
node-version-file: ".node-version"
|
||||||
|
package-manager-cache: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
- name: Run semantic release
|
||||||
|
run: npm run semantic-release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
|
||||||
|
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||||
|
GIT_COMMITTER_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
|
||||||
|
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||||
|
- name: Get version from tag
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
TAG=$(git describe --tags --abbrev=0)
|
||||||
|
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||||
- name: Build app
|
- name: Build app
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
run: npm run build
|
run: npm run build
|
||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
|
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
|
||||||
with:
|
with:
|
||||||
publish-dir: dist
|
publish-dir: dist
|
||||||
deploy-message: 'Prod deploy ${{ github.ref_name }}'
|
deploy-message: 'Prod deploy ${{ steps.vars.outputs.tag }}'
|
||||||
enable-commit-comment: false
|
enable-commit-comment: false
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
production-deploy: true
|
production-deploy: true
|
||||||
@@ -36,9 +52,6 @@ jobs:
|
|||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }}
|
||||||
timeout-minutes: 1
|
timeout-minutes: 1
|
||||||
- name: Get version from tag
|
|
||||||
id: vars
|
|
||||||
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
|
|
||||||
- name: Create tar.gz
|
- name: Create tar.gz
|
||||||
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
|
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
|
||||||
- name: Sign tar.gz
|
- name: Sign tar.gz
|
||||||
@@ -52,45 +65,54 @@ jobs:
|
|||||||
gpg --export | xxd -p
|
gpg --export | xxd -p
|
||||||
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||||
- name: Upload tagged release
|
- name: Upload tagged release
|
||||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||||
with:
|
with:
|
||||||
|
tag_name: ${{ steps.vars.outputs.tag }}
|
||||||
files: |
|
files: |
|
||||||
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||||
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
|
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
|
||||||
|
|
||||||
publish-image:
|
publish-image:
|
||||||
name: Push Docker image to Docker Hub, ghcr
|
name: Push Docker image to Docker Hub, GHCR
|
||||||
|
needs: deploy-and-tarball
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.deploy-and-tarball.outputs.version }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3.6.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.11.1
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub #Do not update this action from a outside PR
|
||||||
uses: docker/login-action@v3.5.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Login to the Container registry
|
- name: Login to the Github Container registry #Do not update this action from a outside PR
|
||||||
uses: docker/login-action@v3.5.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker, GHCR
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5.8.0
|
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_USERNAME }}/cinny
|
${{ secrets.DOCKER_USERNAME }}/cinny
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ env.VERSION }}
|
||||||
|
type=raw,value=latest
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6.18.0
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
3
.husky/pre-commit
Normal file
3
.husky/pre-commit
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are commented until we enable lint and typecheck
|
||||||
|
# npx tsc -p tsconfig.json --noEmit
|
||||||
|
# npx lint-staged
|
||||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
24.13.1
|
||||||
@@ -18,7 +18,7 @@ Bug reports and feature suggestions must use descriptive and concise titles and
|
|||||||
## Pull requests
|
## Pull requests
|
||||||
|
|
||||||
> ### Legal Notice
|
> ### 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.
|
> 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](https://github.com/cinnyapp/cla) upon submiting your pull request.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
## Builder
|
## Builder
|
||||||
FROM node:20.12.2-alpine3.18 as builder
|
FROM node:24.13.1-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ RUN npm run build
|
|||||||
|
|
||||||
|
|
||||||
## App
|
## App
|
||||||
FROM nginx:1.29.1-alpine
|
FROM nginx:1.29.8-alpine
|
||||||
|
|
||||||
COPY --from=builder /src/dist /app
|
COPY --from=builder /src/dist /app
|
||||||
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
|
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ A Matrix client focusing primarily on simple, elegant and secure interface. The
|
|||||||
- [Roadmap](https://github.com/orgs/cinnyapp/projects/1)
|
- [Roadmap](https://github.com/orgs/cinnyapp/projects/1)
|
||||||
- [Contributing](./CONTRIBUTING.md)
|
- [Contributing](./CONTRIBUTING.md)
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
We are currently in the [process of replacing the matrix-js-sdk](https://github.com/cinnyapp/cinny/issues/257#issuecomment-3714406704) with our own SDK. As a result, we will not be accepting any pull requests until further notice.
|
||||||
|
Thank you for your understanding.
|
||||||
|
|
||||||
<img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
|
<img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
@@ -83,7 +87,7 @@ mxFo+ioe/ABCufSmyqFye0psX3Sp
|
|||||||
|
|
||||||
## Local development
|
## Local development
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Iron LTS (v20).
|
> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Krypton LTS (v24.13.1).
|
||||||
|
|
||||||
Execute the following commands to start a development server:
|
Execute the following commands to start a development server:
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
18
config.json
18
config.json
@@ -1,13 +1,6 @@
|
|||||||
{
|
{
|
||||||
"defaultHomeserver": 2,
|
"defaultHomeserver": 1,
|
||||||
"homeserverList": [
|
"homeserverList": ["converser.eu", "matrix.org", "mozilla.org", "unredacted.org", "xmr.se"],
|
||||||
"converser.eu",
|
|
||||||
"envs.net",
|
|
||||||
"matrix.org",
|
|
||||||
"monero.social",
|
|
||||||
"mozilla.org",
|
|
||||||
"xmr.se"
|
|
||||||
],
|
|
||||||
"allowCustomHomeservers": true,
|
"allowCustomHomeservers": true,
|
||||||
|
|
||||||
"featuredCommunities": {
|
"featuredCommunities": {
|
||||||
@@ -15,10 +8,11 @@
|
|||||||
"spaces": [
|
"spaces": [
|
||||||
"#cinny-space:matrix.org",
|
"#cinny-space:matrix.org",
|
||||||
"#community:matrix.org",
|
"#community:matrix.org",
|
||||||
"#space:envs.net",
|
"#space:unredacted.org",
|
||||||
"#science-space:matrix.org",
|
"#science-space:matrix.org",
|
||||||
"#libregaming-games:tchncs.de",
|
"#libregaming-games:tchncs.de",
|
||||||
"#mathematics-on:matrix.org"
|
"#mathematics-on:matrix.org",
|
||||||
|
"#stickers-and-emojis:tastytea.de"
|
||||||
],
|
],
|
||||||
"rooms": [
|
"rooms": [
|
||||||
"#cinny:matrix.org",
|
"#cinny:matrix.org",
|
||||||
@@ -28,7 +22,7 @@
|
|||||||
"#PrivSec.dev:arcticfoxes.net",
|
"#PrivSec.dev:arcticfoxes.net",
|
||||||
"#disroot:aria-net.org"
|
"#disroot:aria-net.org"
|
||||||
],
|
],
|
||||||
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
|
"servers": ["matrixrooms.info", "matrix.org", "mozilla.org", "unredacted.org"]
|
||||||
},
|
},
|
||||||
|
|
||||||
"hashRouter": {
|
"hashRouter": {
|
||||||
|
|||||||
7227
package-lock.json
generated
7227
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
72
package.json
72
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cinny",
|
"name": "cinny",
|
||||||
"version": "4.10.2",
|
"version": "4.11.1",
|
||||||
"description": "Yet another matrix client",
|
"description": "Yet another matrix client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -10,11 +10,53 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "yarn check:eslint && yarn check:prettier",
|
"preview": "vite preview",
|
||||||
|
"lint": "npm run check:eslint && npm run check:prettier",
|
||||||
"check:eslint": "eslint src/*",
|
"check:eslint": "eslint src/*",
|
||||||
"check:prettier": "prettier --check .",
|
"check:prettier": "prettier --check .",
|
||||||
"fix:prettier": "prettier --write .",
|
"fix:prettier": "prettier --write .",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"prepare": "husky install",
|
||||||
|
"commit": "git-cz",
|
||||||
|
"semantic-release": "semantic-release"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{ts,tsx,js,jsx}": "eslint",
|
||||||
|
"*": "prettier --ignore-unknown --write"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"commitizen": {
|
||||||
|
"path": "./node_modules/cz-conventional-changelog"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"branches": [
|
||||||
|
"dev"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
[
|
||||||
|
"@semantic-release/exec",
|
||||||
|
{
|
||||||
|
"prepareCmd": "node scripts/update-version.js ${nextRelease.version}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"src/app/features/settings/about/About.tsx",
|
||||||
|
"src/app/pages/auth/AuthFooter.tsx",
|
||||||
|
"src/app/pages/client/WelcomePage.tsx"
|
||||||
|
],
|
||||||
|
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/github"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Ajay Bura",
|
"author": "Ajay Bura",
|
||||||
@@ -43,7 +85,7 @@
|
|||||||
"emojibase-data": "15.3.2",
|
"emojibase-data": "15.3.2",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"focus-trap-react": "10.0.2",
|
"focus-trap-react": "10.0.2",
|
||||||
"folds": "2.4.0",
|
"folds": "2.6.2",
|
||||||
"html-dom-parser": "4.0.0",
|
"html-dom-parser": "4.0.0",
|
||||||
"html-react-parser": "4.2.0",
|
"html-react-parser": "4.2.0",
|
||||||
"i18next": "23.12.2",
|
"i18next": "23.12.2",
|
||||||
@@ -52,9 +94,10 @@
|
|||||||
"immer": "9.0.16",
|
"immer": "9.0.16",
|
||||||
"is-hotkey": "0.2.0",
|
"is-hotkey": "0.2.0",
|
||||||
"jotai": "2.6.0",
|
"jotai": "2.6.0",
|
||||||
"linkify-react": "4.1.3",
|
"linkify-react": "4.3.2",
|
||||||
"linkifyjs": "4.1.3",
|
"linkifyjs": "4.3.2",
|
||||||
"matrix-js-sdk": "38.2.0",
|
"matrix-js-sdk": "38.2.0",
|
||||||
|
"matrix-widget-api": "1.13.0",
|
||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
"pdfjs-dist": "4.2.67",
|
"pdfjs-dist": "4.2.67",
|
||||||
"prismjs": "1.30.0",
|
"prismjs": "1.30.0",
|
||||||
@@ -67,18 +110,21 @@
|
|||||||
"react-google-recaptcha": "2.1.0",
|
"react-google-recaptcha": "2.1.0",
|
||||||
"react-i18next": "15.0.0",
|
"react-i18next": "15.0.0",
|
||||||
"react-range": "1.8.14",
|
"react-range": "1.8.14",
|
||||||
"react-router-dom": "6.20.0",
|
"react-router-dom": "6.30.3",
|
||||||
"sanitize-html": "2.12.1",
|
"sanitize-html": "2.12.1",
|
||||||
"slate": "0.112.0",
|
"slate": "0.123.0",
|
||||||
"slate-dom": "0.112.2",
|
"slate-dom": "0.123.0",
|
||||||
"slate-history": "0.110.3",
|
"slate-history": "0.113.1",
|
||||||
"slate-react": "0.112.1",
|
"slate-react": "0.123.0",
|
||||||
"ua-parser-js": "1.0.35"
|
"ua-parser-js": "1.0.35"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@element-hq/element-call-embedded": "0.16.3",
|
||||||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||||
"@rollup/plugin-inject": "5.0.3",
|
"@rollup/plugin-inject": "5.0.3",
|
||||||
"@rollup/plugin-wasm": "6.1.1",
|
"@rollup/plugin-wasm": "6.1.1",
|
||||||
|
"@semantic-release/exec": "7.1.0",
|
||||||
|
"@semantic-release/git": "10.0.1",
|
||||||
"@types/chroma-js": "3.1.1",
|
"@types/chroma-js": "3.1.1",
|
||||||
"@types/file-saver": "2.0.5",
|
"@types/file-saver": "2.0.5",
|
||||||
"@types/is-hotkey": "0.1.10",
|
"@types/is-hotkey": "0.1.10",
|
||||||
@@ -93,6 +139,7 @@
|
|||||||
"@typescript-eslint/parser": "5.46.1",
|
"@typescript-eslint/parser": "5.46.1",
|
||||||
"@vitejs/plugin-react": "4.2.0",
|
"@vitejs/plugin-react": "4.2.0",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
|
"cz-conventional-changelog": "3.3.0",
|
||||||
"eslint": "8.29.0",
|
"eslint": "8.29.0",
|
||||||
"eslint-config-airbnb": "19.0.4",
|
"eslint-config-airbnb": "19.0.4",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
@@ -100,7 +147,10 @@
|
|||||||
"eslint-plugin-jsx-a11y": "6.6.1",
|
"eslint-plugin-jsx-a11y": "6.6.1",
|
||||||
"eslint-plugin-react": "7.31.11",
|
"eslint-plugin-react": "7.31.11",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
|
"husky": "9.1.7",
|
||||||
|
"lint-staged": "16.3.2",
|
||||||
"prettier": "2.8.1",
|
"prettier": "2.8.1",
|
||||||
|
"semantic-release": "25.0.3",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
"vite": "5.4.19",
|
"vite": "5.4.19",
|
||||||
"vite-plugin-pwa": "0.20.5",
|
"vite-plugin-pwa": "0.20.5",
|
||||||
|
|||||||
48
scripts/update-version.js
Normal file
48
scripts/update-version.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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}`);
|
||||||
|
});
|
||||||
@@ -51,8 +51,12 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
|||||||
},
|
},
|
||||||
location.pathname
|
location.pathname
|
||||||
);
|
);
|
||||||
if (spaceMatch?.params.spaceIdOrAlias) {
|
const encodedSpaceIdOrAlias = spaceMatch?.params.spaceIdOrAlias;
|
||||||
navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias));
|
const decodedSpaceIdOrAlias =
|
||||||
|
encodedSpaceIdOrAlias && decodeURIComponent(encodedSpaceIdOrAlias);
|
||||||
|
|
||||||
|
if (decodedSpaceIdOrAlias) {
|
||||||
|
navigate(getSpacePath(decodedSpaceIdOrAlias));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|||||||
66
src/app/components/CallEmbedProvider.tsx
Normal file
66
src/app/components/CallEmbedProvider.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React, { ReactNode, useCallback, useRef } from 'react';
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
|
import { config } from 'folds';
|
||||||
|
import {
|
||||||
|
CallEmbedContextProvider,
|
||||||
|
CallEmbedRefContextProvider,
|
||||||
|
useCallHangupEvent,
|
||||||
|
useCallJoined,
|
||||||
|
useCallThemeSync,
|
||||||
|
useCallMemberSoundSync,
|
||||||
|
} from '../hooks/useCallEmbed';
|
||||||
|
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
|
||||||
|
import { CallEmbed } from '../plugins/call';
|
||||||
|
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
||||||
|
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
||||||
|
|
||||||
|
function CallUtils({ embed }: { embed: CallEmbed }) {
|
||||||
|
const setCallEmbed = useSetAtom(callEmbedAtom);
|
||||||
|
|
||||||
|
useCallMemberSoundSync(embed);
|
||||||
|
useCallThemeSync(embed);
|
||||||
|
useCallHangupEvent(
|
||||||
|
embed,
|
||||||
|
useCallback(() => {
|
||||||
|
setCallEmbed(undefined);
|
||||||
|
}, [setCallEmbed])
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallEmbedProviderProps = {
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||||
|
const callEmbed = useAtomValue(callEmbedAtom);
|
||||||
|
const callEmbedRef = useRef<HTMLDivElement>(null);
|
||||||
|
const joined = useCallJoined(callEmbed);
|
||||||
|
|
||||||
|
const selectedRoom = useSelectedRoom();
|
||||||
|
const chat = useAtomValue(callChatAtom);
|
||||||
|
const screenSize = useScreenSizeContext();
|
||||||
|
|
||||||
|
const chatOnlyView = chat && screenSize !== ScreenSize.Desktop;
|
||||||
|
|
||||||
|
const callVisible = callEmbed && selectedRoom === callEmbed.roomId && joined && !chatOnlyView;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CallEmbedContextProvider value={callEmbed}>
|
||||||
|
{callEmbed && <CallUtils embed={callEmbed} />}
|
||||||
|
<CallEmbedRefContextProvider value={callEmbedRef}>{children}</CallEmbedRefContextProvider>
|
||||||
|
<div
|
||||||
|
data-call-embed-container
|
||||||
|
style={{
|
||||||
|
visibility: callVisible ? undefined : 'hidden',
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '50%',
|
||||||
|
}}
|
||||||
|
ref={callEmbedRef}
|
||||||
|
/>
|
||||||
|
</CallEmbedContextProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
src/app/components/ImageOverlay.tsx
Normal file
45
src/app/components/ImageOverlay.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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) => (
|
||||||
|
<Overlay {...props} ref={ref} open={viewer} backdrop={<OverlayBackdrop />}>
|
||||||
|
<OverlayCenter>
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => requestClose(),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Modal
|
||||||
|
className={ModalWide}
|
||||||
|
size="500"
|
||||||
|
onContextMenu={(evt: any) => evt.stopPropagation()}
|
||||||
|
>
|
||||||
|
{renderViewer({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
requestClose,
|
||||||
|
})}
|
||||||
|
</Modal>
|
||||||
|
</FocusTrap>
|
||||||
|
</OverlayCenter>
|
||||||
|
</Overlay>
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -16,34 +16,24 @@ import {
|
|||||||
import { JoinRule } from 'matrix-js-sdk';
|
import { JoinRule } from 'matrix-js-sdk';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { stopPropagation } from '../utils/keyboard';
|
import { stopPropagation } from '../utils/keyboard';
|
||||||
|
import { getRoomIconSrc } from '../utils/room';
|
||||||
|
|
||||||
export type ExtraJoinRules = 'knock_restricted';
|
export type ExtraJoinRules = 'knock_restricted';
|
||||||
export type ExtendedJoinRules = JoinRule | ExtraJoinRules;
|
export type ExtendedJoinRules = JoinRule | ExtraJoinRules;
|
||||||
|
|
||||||
type JoinRuleIcons = Record<ExtendedJoinRules, IconSrc>;
|
type JoinRuleIcons = Record<ExtendedJoinRules, IconSrc>;
|
||||||
export const useRoomJoinRuleIcon = (): JoinRuleIcons =>
|
|
||||||
|
export const useJoinRuleIcons = (roomType?: string): JoinRuleIcons =>
|
||||||
useMemo(
|
useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
[JoinRule.Invite]: Icons.HashLock,
|
[JoinRule.Invite]: getRoomIconSrc(Icons, roomType, JoinRule.Invite),
|
||||||
[JoinRule.Knock]: Icons.HashLock,
|
[JoinRule.Knock]: getRoomIconSrc(Icons, roomType, JoinRule.Knock),
|
||||||
knock_restricted: Icons.Hash,
|
knock_restricted: getRoomIconSrc(Icons, roomType, JoinRule.Restricted),
|
||||||
[JoinRule.Restricted]: Icons.Hash,
|
[JoinRule.Restricted]: getRoomIconSrc(Icons, roomType, JoinRule.Restricted),
|
||||||
[JoinRule.Public]: Icons.HashGlobe,
|
[JoinRule.Public]: getRoomIconSrc(Icons, roomType, JoinRule.Public),
|
||||||
[JoinRule.Private]: Icons.HashLock,
|
[JoinRule.Private]: getRoomIconSrc(Icons, roomType, JoinRule.Private),
|
||||||
}),
|
}),
|
||||||
[]
|
[roomType]
|
||||||
);
|
|
||||||
export const useSpaceJoinRuleIcon = (): JoinRuleIcons =>
|
|
||||||
useMemo(
|
|
||||||
() => ({
|
|
||||||
[JoinRule.Invite]: Icons.SpaceLock,
|
|
||||||
[JoinRule.Knock]: Icons.SpaceLock,
|
|
||||||
knock_restricted: Icons.Space,
|
|
||||||
[JoinRule.Restricted]: Icons.Space,
|
|
||||||
[JoinRule.Public]: Icons.SpaceGlobe,
|
|
||||||
[JoinRule.Private]: Icons.SpaceLock,
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
type JoinRuleLabels = Record<ExtendedJoinRules, string>;
|
type JoinRuleLabels = Record<ExtendedJoinRules, string>;
|
||||||
|
|||||||
@@ -2,43 +2,39 @@ import React from 'react';
|
|||||||
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
|
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
|
||||||
import { SequenceCard } from '../sequence-card';
|
import { SequenceCard } from '../sequence-card';
|
||||||
import { SettingTile } from '../setting-tile';
|
import { SettingTile } from '../setting-tile';
|
||||||
|
import { CreateRoomAccess } from './types';
|
||||||
|
|
||||||
export enum CreateRoomKind {
|
type CreateRoomAccessSelectorProps = {
|
||||||
Private = 'private',
|
value?: CreateRoomAccess;
|
||||||
Restricted = 'restricted',
|
onSelect: (value: CreateRoomAccess) => void;
|
||||||
Public = 'public',
|
|
||||||
}
|
|
||||||
type CreateRoomKindSelectorProps = {
|
|
||||||
value?: CreateRoomKind;
|
|
||||||
onSelect: (value: CreateRoomKind) => void;
|
|
||||||
canRestrict?: boolean;
|
canRestrict?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
getIcon: (kind: CreateRoomKind) => IconSrc;
|
getIcon: (access: CreateRoomAccess) => IconSrc;
|
||||||
};
|
};
|
||||||
export function CreateRoomKindSelector({
|
export function CreateRoomAccessSelector({
|
||||||
value,
|
value,
|
||||||
onSelect,
|
onSelect,
|
||||||
canRestrict,
|
canRestrict,
|
||||||
disabled,
|
disabled,
|
||||||
getIcon,
|
getIcon,
|
||||||
}: CreateRoomKindSelectorProps) {
|
}: CreateRoomAccessSelectorProps) {
|
||||||
return (
|
return (
|
||||||
<Box shrink="No" direction="Column" gap="100">
|
<Box shrink="No" direction="Column" gap="100">
|
||||||
{canRestrict && (
|
{canRestrict && (
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
style={{ padding: config.space.S300 }}
|
style={{ padding: config.space.S300 }}
|
||||||
variant={value === CreateRoomKind.Restricted ? 'Primary' : 'SurfaceVariant'}
|
variant={value === CreateRoomAccess.Restricted ? 'Primary' : 'SurfaceVariant'}
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="100"
|
gap="100"
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
aria-pressed={value === CreateRoomKind.Restricted}
|
aria-pressed={value === CreateRoomAccess.Restricted}
|
||||||
onClick={() => onSelect(CreateRoomKind.Restricted)}
|
onClick={() => onSelect(CreateRoomAccess.Restricted)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
before={<Icon size="400" src={getIcon(CreateRoomKind.Restricted)} />}
|
before={<Icon size="400" src={getIcon(CreateRoomAccess.Restricted)} />}
|
||||||
after={value === CreateRoomKind.Restricted && <Icon src={Icons.Check} />}
|
after={value === CreateRoomAccess.Restricted && <Icon src={Icons.Check} />}
|
||||||
>
|
>
|
||||||
<Text size="H6">Restricted</Text>
|
<Text size="H6">Restricted</Text>
|
||||||
<Text size="T300" priority="300">
|
<Text size="T300" priority="300">
|
||||||
@@ -49,18 +45,18 @@ export function CreateRoomKindSelector({
|
|||||||
)}
|
)}
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
style={{ padding: config.space.S300 }}
|
style={{ padding: config.space.S300 }}
|
||||||
variant={value === CreateRoomKind.Private ? 'Primary' : 'SurfaceVariant'}
|
variant={value === CreateRoomAccess.Private ? 'Primary' : 'SurfaceVariant'}
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="100"
|
gap="100"
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
aria-pressed={value === CreateRoomKind.Private}
|
aria-pressed={value === CreateRoomAccess.Private}
|
||||||
onClick={() => onSelect(CreateRoomKind.Private)}
|
onClick={() => onSelect(CreateRoomAccess.Private)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
before={<Icon size="400" src={getIcon(CreateRoomKind.Private)} />}
|
before={<Icon size="400" src={getIcon(CreateRoomAccess.Private)} />}
|
||||||
after={value === CreateRoomKind.Private && <Icon src={Icons.Check} />}
|
after={value === CreateRoomAccess.Private && <Icon src={Icons.Check} />}
|
||||||
>
|
>
|
||||||
<Text size="H6">Private</Text>
|
<Text size="H6">Private</Text>
|
||||||
<Text size="T300" priority="300">
|
<Text size="T300" priority="300">
|
||||||
@@ -70,18 +66,18 @@ export function CreateRoomKindSelector({
|
|||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
style={{ padding: config.space.S300 }}
|
style={{ padding: config.space.S300 }}
|
||||||
variant={value === CreateRoomKind.Public ? 'Primary' : 'SurfaceVariant'}
|
variant={value === CreateRoomAccess.Public ? 'Primary' : 'SurfaceVariant'}
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="100"
|
gap="100"
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
aria-pressed={value === CreateRoomKind.Public}
|
aria-pressed={value === CreateRoomAccess.Public}
|
||||||
onClick={() => onSelect(CreateRoomKind.Public)}
|
onClick={() => onSelect(CreateRoomAccess.Public)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
before={<Icon size="400" src={getIcon(CreateRoomKind.Public)} />}
|
before={<Icon size="400" src={getIcon(CreateRoomAccess.Public)} />}
|
||||||
after={value === CreateRoomKind.Public && <Icon src={Icons.Check} />}
|
after={value === CreateRoomAccess.Public && <Icon src={Icons.Check} />}
|
||||||
>
|
>
|
||||||
<Text size="H6">Public</Text>
|
<Text size="H6">Public</Text>
|
||||||
<Text size="T300" priority="300">
|
<Text size="T300" priority="300">
|
||||||
75
src/app/components/create-room/CreateRoomTypeSelector.tsx
Normal file
75
src/app/components/create-room/CreateRoomTypeSelector.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
|
||||||
|
import { SequenceCard } from '../sequence-card';
|
||||||
|
import { SettingTile } from '../setting-tile';
|
||||||
|
import { CreateRoomType } from './types';
|
||||||
|
import { BetaNoticeBadge } from '../BetaNoticeBadge';
|
||||||
|
|
||||||
|
type CreateRoomTypeSelectorProps = {
|
||||||
|
value?: CreateRoomType;
|
||||||
|
onSelect: (value: CreateRoomType) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
getIcon: (type: CreateRoomType) => IconSrc;
|
||||||
|
};
|
||||||
|
export function CreateRoomTypeSelector({
|
||||||
|
value,
|
||||||
|
onSelect,
|
||||||
|
disabled,
|
||||||
|
getIcon,
|
||||||
|
}: CreateRoomTypeSelectorProps) {
|
||||||
|
return (
|
||||||
|
<Box shrink="No" direction="Column" gap="100">
|
||||||
|
<SequenceCard
|
||||||
|
style={{ padding: config.space.S300 }}
|
||||||
|
variant={value === CreateRoomType.TextRoom ? 'Primary' : 'SurfaceVariant'}
|
||||||
|
direction="Column"
|
||||||
|
gap="100"
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
aria-pressed={value === CreateRoomType.TextRoom}
|
||||||
|
onClick={() => onSelect(CreateRoomType.TextRoom)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SettingTile
|
||||||
|
before={<Icon size="400" src={getIcon(CreateRoomType.TextRoom)} />}
|
||||||
|
after={value === CreateRoomType.TextRoom && <Icon src={Icons.Check} />}
|
||||||
|
>
|
||||||
|
<Box gap="200" alignItems="Baseline">
|
||||||
|
<Text size="H6" style={{ flexShrink: 0 }}>
|
||||||
|
Chat Room
|
||||||
|
</Text>
|
||||||
|
<Text size="T300" priority="300" truncate>
|
||||||
|
- Messages, photos, and videos.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</SettingTile>
|
||||||
|
</SequenceCard>
|
||||||
|
<SequenceCard
|
||||||
|
style={{ padding: config.space.S300 }}
|
||||||
|
variant={value === CreateRoomType.VoiceRoom ? 'Primary' : 'SurfaceVariant'}
|
||||||
|
direction="Column"
|
||||||
|
gap="100"
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
aria-pressed={value === CreateRoomType.VoiceRoom}
|
||||||
|
onClick={() => onSelect(CreateRoomType.VoiceRoom)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SettingTile
|
||||||
|
before={<Icon size="400" src={getIcon(CreateRoomType.VoiceRoom)} />}
|
||||||
|
after={value === CreateRoomType.VoiceRoom && <Icon src={Icons.Check} />}
|
||||||
|
>
|
||||||
|
<Box gap="200" alignItems="Baseline">
|
||||||
|
<Text size="H6" style={{ flexShrink: 0 }}>
|
||||||
|
Voice Room
|
||||||
|
</Text>
|
||||||
|
<Text size="T300" priority="300" truncate>
|
||||||
|
- Live audio and video conversations.
|
||||||
|
</Text>
|
||||||
|
<BetaNoticeBadge />
|
||||||
|
</Box>
|
||||||
|
</SettingTile>
|
||||||
|
</SequenceCard>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './CreateRoomKindSelector';
|
export * from './CreateRoomAccessSelector';
|
||||||
export * from './CreateRoomAliasInput';
|
export * from './CreateRoomAliasInput';
|
||||||
export * from './RoomVersionSelector';
|
export * from './RoomVersionSelector';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './AdditionalCreatorInput';
|
export * from './AdditionalCreatorInput';
|
||||||
|
export * from './types';
|
||||||
|
|||||||
10
src/app/components/create-room/types.ts
Normal file
10
src/app/components/create-room/types.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export enum CreateRoomType {
|
||||||
|
TextRoom = 'text',
|
||||||
|
VoiceRoom = 'voice',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CreateRoomAccess {
|
||||||
|
Private = 'private',
|
||||||
|
Restricted = 'restricted',
|
||||||
|
Public = 'public',
|
||||||
|
}
|
||||||
@@ -7,10 +7,10 @@ import {
|
|||||||
Room,
|
Room,
|
||||||
} from 'matrix-js-sdk';
|
} from 'matrix-js-sdk';
|
||||||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import { CreateRoomKind } from './CreateRoomKindSelector';
|
|
||||||
import { RoomType, StateEvent } from '../../../types/matrix/room';
|
import { RoomType, StateEvent } from '../../../types/matrix/room';
|
||||||
import { getViaServers } from '../../plugins/via-servers';
|
import { getViaServers } from '../../plugins/via-servers';
|
||||||
import { getMxIdServer } from '../../utils/matrix';
|
import { getMxIdServer } from '../../utils/matrix';
|
||||||
|
import { CreateRoomAccess } from './types';
|
||||||
|
|
||||||
export const createRoomCreationContent = (
|
export const createRoomCreationContent = (
|
||||||
type: RoomType | undefined,
|
type: RoomType | undefined,
|
||||||
@@ -32,7 +32,7 @@ export const createRoomCreationContent = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const createRoomJoinRulesState = (
|
export const createRoomJoinRulesState = (
|
||||||
kind: CreateRoomKind,
|
access: CreateRoomAccess,
|
||||||
parent: Room | undefined,
|
parent: Room | undefined,
|
||||||
knock: boolean
|
knock: boolean
|
||||||
) => {
|
) => {
|
||||||
@@ -40,13 +40,13 @@ export const createRoomJoinRulesState = (
|
|||||||
join_rule: knock ? JoinRule.Knock : JoinRule.Invite,
|
join_rule: knock ? JoinRule.Knock : JoinRule.Invite,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (kind === CreateRoomKind.Public) {
|
if (access === CreateRoomAccess.Public) {
|
||||||
content = {
|
content = {
|
||||||
join_rule: JoinRule.Public,
|
join_rule: JoinRule.Public,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kind === CreateRoomKind.Restricted && parent) {
|
if (access === CreateRoomAccess.Restricted && parent) {
|
||||||
content = {
|
content = {
|
||||||
join_rule: knock ? ('knock_restricted' as JoinRule) : JoinRule.Restricted,
|
join_rule: knock ? ('knock_restricted' as JoinRule) : JoinRule.Restricted,
|
||||||
allow: [
|
allow: [
|
||||||
@@ -74,6 +74,10 @@ export const createRoomParentState = (parent: Room) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createSpacePowerLevelsOverride = () => ({
|
||||||
|
events_default: 50,
|
||||||
|
});
|
||||||
|
|
||||||
export const createRoomEncryptionState = () => ({
|
export const createRoomEncryptionState = () => ({
|
||||||
type: 'm.room.encryption',
|
type: 'm.room.encryption',
|
||||||
state_key: '',
|
state_key: '',
|
||||||
@@ -82,11 +86,23 @@ export const createRoomEncryptionState = () => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const createRoomCallState = () => ({
|
||||||
|
type: 'org.matrix.msc3401.call',
|
||||||
|
state_key: '',
|
||||||
|
content: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createVoiceRoomPowerLevelsOverride = () => ({
|
||||||
|
events: {
|
||||||
|
[StateEvent.GroupCallMemberPrefix]: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export type CreateRoomData = {
|
export type CreateRoomData = {
|
||||||
version: string;
|
version: string;
|
||||||
type?: RoomType;
|
type?: RoomType;
|
||||||
parent?: Room;
|
parent?: Room;
|
||||||
kind: CreateRoomKind;
|
access: CreateRoomAccess;
|
||||||
name: string;
|
name: string;
|
||||||
topic?: string;
|
topic?: string;
|
||||||
aliasLocalPart?: string;
|
aliasLocalPart?: string;
|
||||||
@@ -106,7 +122,11 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
|
|||||||
initialState.push(createRoomParentState(data.parent));
|
initialState.push(createRoomParentState(data.parent));
|
||||||
}
|
}
|
||||||
|
|
||||||
initialState.push(createRoomJoinRulesState(data.kind, data.parent, data.knock));
|
if (data.type === RoomType.Call) {
|
||||||
|
initialState.push(createRoomCallState());
|
||||||
|
}
|
||||||
|
|
||||||
|
initialState.push(createRoomJoinRulesState(data.access, data.parent, data.knock));
|
||||||
|
|
||||||
const options: ICreateRoomOpts = {
|
const options: ICreateRoomOpts = {
|
||||||
room_version: data.version,
|
room_version: data.version,
|
||||||
@@ -118,9 +138,15 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
|
|||||||
data.allowFederation,
|
data.allowFederation,
|
||||||
data.additionalCreators
|
data.additionalCreators
|
||||||
),
|
),
|
||||||
|
power_level_content_override:
|
||||||
|
data.type === RoomType.Call ? createVoiceRoomPowerLevelsOverride() : undefined,
|
||||||
initial_state: initialState,
|
initial_state: initialState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (data.type === RoomType.Space) {
|
||||||
|
options.power_level_content_override = createSpacePowerLevelsOverride();
|
||||||
|
}
|
||||||
|
|
||||||
const result = await mx.createRoom(options);
|
const result = await mx.createRoom(options);
|
||||||
|
|
||||||
if (data.parent) {
|
if (data.parent) {
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ export function EmoticonAutocomplete({
|
|||||||
{autoCompleteEmoticon.map((emoticon) => {
|
{autoCompleteEmoticon.map((emoticon) => {
|
||||||
const isCustomEmoji = 'url' in emoticon;
|
const isCustomEmoji = 'url' in emoticon;
|
||||||
const key = isCustomEmoji ? emoticon.url : emoticon.unicode;
|
const key = isCustomEmoji ? emoticon.url : emoticon.unicode;
|
||||||
|
const customEmojiUrl = mxcUrlToHttp(mx, key, useAuthentication);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={emoticon.shortcode + key}
|
key={emoticon.shortcode + key}
|
||||||
@@ -98,11 +100,11 @@ export function EmoticonAutocomplete({
|
|||||||
}
|
}
|
||||||
onClick={() => handleAutocomplete(key, emoticon.shortcode)}
|
onClick={() => handleAutocomplete(key, emoticon.shortcode)}
|
||||||
before={
|
before={
|
||||||
isCustomEmoji ? (
|
isCustomEmoji && customEmojiUrl ? (
|
||||||
<Box
|
<Box
|
||||||
shrink="No"
|
shrink="No"
|
||||||
as="img"
|
as="img"
|
||||||
src={mxcUrlToHttp(mx, key, useAuthentication) || key}
|
src={customEmojiUrl}
|
||||||
alt={emoticon.shortcode}
|
alt={emoticon.shortcode}
|
||||||
style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }}
|
style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -169,12 +169,13 @@ export function RoomMentionAutocomplete({
|
|||||||
<RoomIcon
|
<RoomIcon
|
||||||
size="50"
|
size="50"
|
||||||
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
|
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
|
||||||
|
roomType={room.getType()}
|
||||||
filled
|
filled
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RoomIcon size="100" joinRule={room.getJoinRule()} space={room.isSpaceRoom()} />
|
<RoomIcon size="100" joinRule={room.getJoinRule()} roomType={room.getType()} />
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,9 +228,13 @@ const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElemen
|
|||||||
children: [{ text }],
|
children: [{ text }],
|
||||||
}));
|
}));
|
||||||
const childCode = node.children[0];
|
const childCode = node.children[0];
|
||||||
const className =
|
const attribs =
|
||||||
isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs.class ?? '' : '';
|
isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs : undefined;
|
||||||
const prefix = { text: `${mdSequence}${className.replace('language-', '')}` };
|
const languageClass = attribs?.class;
|
||||||
|
const customLabel = attribs?.['data-label'];
|
||||||
|
const prefix = {
|
||||||
|
text: `${mdSequence}${customLabel ?? languageClass?.replace('language-', '') ?? ''}`,
|
||||||
|
};
|
||||||
const suffix = { text: mdSequence };
|
const suffix = { text: mdSequence };
|
||||||
return [
|
return [
|
||||||
{ type: BlockType.Paragraph, children: [prefix] },
|
{ type: BlockType.Paragraph, children: [prefix] },
|
||||||
|
|||||||
@@ -212,9 +212,10 @@ export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): M
|
|||||||
if (node.type === BlockType.CodeBlock) return;
|
if (node.type === BlockType.CodeBlock) return;
|
||||||
|
|
||||||
if (node.type === BlockType.Mention) {
|
if (node.type === BlockType.Mention) {
|
||||||
if (node.id === getCanonicalAliasOrRoomId(mx, roomId)) {
|
if (node.name === '@room') {
|
||||||
mentionData.room = true;
|
mentionData.room = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUserId(node.id) && node.id !== mx.getUserId()) {
|
if (isUserId(node.id) && node.id !== mx.getUserId()) {
|
||||||
mentionData.users.add(node.id);
|
mentionData.users.add(node.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,8 +202,7 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
|
|||||||
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
|
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ||
|
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined;
|
||||||
pack.meta.avatar;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageGroupIcon
|
<ImageGroupIcon
|
||||||
@@ -266,7 +265,7 @@ function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSide
|
|||||||
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
|
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || pack.meta.avatar;
|
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageGroupIcon
|
<ImageGroupIcon
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiIte
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
className={css.CustomEmojiImg}
|
className={css.CustomEmojiImg}
|
||||||
alt={image.body || image.shortcode}
|
alt={image.body || image.shortcode}
|
||||||
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
|
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -98,7 +98,7 @@ export function StickerItem({ mx, useAuthentication, image }: StickerItemProps)
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
className={css.StickerImg}
|
className={css.StickerImg}
|
||||||
alt={image.body || image.shortcode}
|
alt={image.body || image.shortcode}
|
||||||
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
|
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow
|
|||||||
|
|
||||||
const [downloadState, download] = useAsyncCallback(
|
const [downloadState, download] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
||||||
|
if (!mediaUrl) throw new Error('Invalid media URL');
|
||||||
const fileContent = encInfo
|
const fileContent = encInfo
|
||||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
||||||
: await downloadMedia(mediaUrl);
|
: await downloadMedia(mediaUrl);
|
||||||
|
|||||||
@@ -389,6 +389,8 @@ export function MLocation({ content }: MLocationProps) {
|
|||||||
const geoUri = content.geo_uri;
|
const geoUri = content.geo_uri;
|
||||||
if (typeof geoUri !== 'string') return <BrokenContent />;
|
if (typeof geoUri !== 'string') return <BrokenContent />;
|
||||||
const location = parseGeoUri(geoUri);
|
const location = parseGeoUri(geoUri);
|
||||||
|
if (!location) return <BrokenContent />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" alignItems="Start" gap="100">
|
<Box direction="Column" alignItems="Start" gap="100">
|
||||||
<Text size="T400">{geoUri}</Text>
|
<Text size="T400">{geoUri}</Text>
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ export function AudioContent({
|
|||||||
|
|
||||||
const [srcState, loadSrc] = useAsyncCallback(
|
const [srcState, loadSrc] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
||||||
|
if (!mediaUrl) throw new Error('Invalid media URL');
|
||||||
const fileContent = encInfo
|
const fileContent = encInfo
|
||||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
||||||
: await downloadMedia(mediaUrl);
|
: await downloadMedia(mediaUrl);
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
|
|||||||
|
|
||||||
const [textState, loadText] = useAsyncCallback(
|
const [textState, loadText] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
||||||
|
if (!mediaUrl) throw new Error('Invalid media URL');
|
||||||
const fileContent = encInfo
|
const fileContent = encInfo
|
||||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
||||||
: await downloadMedia(mediaUrl);
|
: await downloadMedia(mediaUrl);
|
||||||
@@ -176,7 +177,8 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
|
|||||||
|
|
||||||
const [pdfState, loadPdf] = useAsyncCallback(
|
const [pdfState, loadPdf] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
||||||
|
if (!mediaUrl) throw new Error('Invalid media URL');
|
||||||
const fileContent = encInfo
|
const fileContent = encInfo
|
||||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
||||||
: await downloadMedia(mediaUrl);
|
: await downloadMedia(mediaUrl);
|
||||||
@@ -253,7 +255,8 @@ export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFil
|
|||||||
|
|
||||||
const [downloadState, download] = useAsyncCallback(
|
const [downloadState, download] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
||||||
|
if (!mediaUrl) throw new Error('Invalid media URL');
|
||||||
const fileContent = encInfo
|
const fileContent = encInfo
|
||||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
||||||
: await downloadMedia(mediaUrl);
|
: await downloadMedia(mediaUrl);
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ export const ImageContent = as<'div', ImageContentProps>(
|
|||||||
|
|
||||||
const [srcState, loadSrc] = useAsyncCallback(
|
const [srcState, loadSrc] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
||||||
|
if (!mediaUrl) throw new Error('Invalid media URL');
|
||||||
if (encInfo) {
|
if (encInfo) {
|
||||||
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
||||||
decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo)
|
decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo)
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) {
|
|||||||
throw new Error('Failed to load thumbnail');
|
throw new Error('Failed to load thumbnail');
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? thumbMxcUrl;
|
const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication);
|
||||||
|
if (!mediaUrl) throw new Error('Invalid media URL');
|
||||||
if (encInfo) {
|
if (encInfo) {
|
||||||
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
||||||
decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo)
|
decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo)
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ export const VideoContent = as<'div', VideoContentProps>(
|
|||||||
|
|
||||||
const [srcState, loadSrc] = useAsyncCallback(
|
const [srcState, loadSrc] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
||||||
|
if (!mediaUrl) throw new Error('Invalid media URL');
|
||||||
const fileContent = encInfo
|
const fileContent = encInfo
|
||||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
||||||
decryptFile(encBuf, mimeType, encInfo)
|
decryptFile(encBuf, mimeType, encInfo)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { JoinRule } from 'matrix-js-sdk';
|
|||||||
import { AvatarFallback, AvatarImage, Icon, Icons, color } from 'folds';
|
import { AvatarFallback, AvatarImage, Icon, Icons, color } from 'folds';
|
||||||
import React, { ComponentProps, ReactEventHandler, ReactNode, forwardRef, useState } from 'react';
|
import React, { ComponentProps, ReactEventHandler, ReactNode, forwardRef, useState } from 'react';
|
||||||
import * as css from './RoomAvatar.css';
|
import * as css from './RoomAvatar.css';
|
||||||
import { joinRuleToIconSrc } from '../../utils/room';
|
import { getRoomIconSrc } from '../../utils/room';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
|
||||||
type RoomAvatarProps = {
|
type RoomAvatarProps = {
|
||||||
@@ -44,13 +44,9 @@ export function RoomAvatar({ roomId, src, alt, renderFallback }: RoomAvatarProps
|
|||||||
export const RoomIcon = forwardRef<
|
export const RoomIcon = forwardRef<
|
||||||
SVGSVGElement,
|
SVGSVGElement,
|
||||||
Omit<ComponentProps<typeof Icon>, 'src'> & {
|
Omit<ComponentProps<typeof Icon>, 'src'> & {
|
||||||
joinRule: JoinRule;
|
joinRule?: JoinRule;
|
||||||
space?: boolean;
|
roomType?: string;
|
||||||
}
|
}
|
||||||
>(({ joinRule, space, ...props }, ref) => (
|
>(({ joinRule, roomType, ...props }, ref) => (
|
||||||
<Icon
|
<Icon src={getRoomIconSrc(Icons, roomType, joinRule)} {...props} ref={ref} />
|
||||||
src={joinRuleToIconSrc(Icons, joinRule, space || false) ?? Icons.Hash}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const SequenceCard = as<
|
|||||||
firstChild,
|
firstChild,
|
||||||
lastChild,
|
lastChild,
|
||||||
outlined,
|
outlined,
|
||||||
|
mergeBorder,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@@ -24,7 +25,7 @@ export const SequenceCard = as<
|
|||||||
<Box
|
<Box
|
||||||
as={AsSequenceCard}
|
as={AsSequenceCard}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
css.SequenceCard({ radii, outlined }),
|
css.SequenceCard({ radii, outlined, mergeBorder }),
|
||||||
ContainerColor({ variant }),
|
ContainerColor({ variant }),
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const SequenceCard = recipe({
|
|||||||
},
|
},
|
||||||
borderStyle: 'solid',
|
borderStyle: 'solid',
|
||||||
borderWidth: outlinedWidth,
|
borderWidth: outlinedWidth,
|
||||||
borderBottomWidth: 0,
|
|
||||||
selectors: {
|
selectors: {
|
||||||
'&:first-child, :not(&) + &': {
|
'&:first-child, :not(&) + &': {
|
||||||
borderTopLeftRadius: [radii],
|
borderTopLeftRadius: [radii],
|
||||||
@@ -20,7 +20,6 @@ export const SequenceCard = recipe({
|
|||||||
'&:last-child, &:not(:has(+&))': {
|
'&:last-child, &:not(:has(+&))': {
|
||||||
borderBottomLeftRadius: [radii],
|
borderBottomLeftRadius: [radii],
|
||||||
borderBottomRightRadius: [radii],
|
borderBottomRightRadius: [radii],
|
||||||
borderBottomWidth: outlinedWidth,
|
|
||||||
},
|
},
|
||||||
[`&[data-first-child="true"]`]: {
|
[`&[data-first-child="true"]`]: {
|
||||||
borderTopLeftRadius: [radii],
|
borderTopLeftRadius: [radii],
|
||||||
@@ -74,6 +73,16 @@ export const SequenceCard = recipe({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mergeBorder: {
|
||||||
|
true: {
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
selectors: {
|
||||||
|
'&:last-child, &:not(:has(+&))': {
|
||||||
|
borderBottomWidth: outlinedWidth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
radii: '400',
|
radii: '400',
|
||||||
|
|||||||
18
src/app/components/stacked-avatar/StackedAvatar.tsx
Normal file
18
src/app/components/stacked-avatar/StackedAvatar.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { as, Avatar } from 'folds';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
|
||||||
|
type StackedAvatarProps = {
|
||||||
|
radii?: '0' | '300' | '400' | '500' | 'Pill' | 'Inherit' | undefined;
|
||||||
|
};
|
||||||
|
export const StackedAvatar = as<'span', css.StackedAvatarVariants & StackedAvatarProps>(
|
||||||
|
({ size, variant, className, ...props }, ref) => (
|
||||||
|
<Avatar
|
||||||
|
size={size}
|
||||||
|
className={classNames(css.StackedAvatar({ size, variant }), className)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
1
src/app/components/stacked-avatar/index.ts
Normal file
1
src/app/components/stacked-avatar/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './StackedAvatar';
|
||||||
59
src/app/components/stacked-avatar/styles.css.ts
Normal file
59
src/app/components/stacked-avatar/styles.css.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { ComplexStyleRule } from '@vanilla-extract/css';
|
||||||
|
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
||||||
|
import { color, config, ContainerColor, toRem } from 'folds';
|
||||||
|
|
||||||
|
const getVariant = (variant: ContainerColor): ComplexStyleRule => ({
|
||||||
|
outlineColor: color[variant].Container,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StackedAvatar = recipe({
|
||||||
|
base: {
|
||||||
|
backgroundColor: color.Surface.Container,
|
||||||
|
outlineStyle: 'solid',
|
||||||
|
selectors: {
|
||||||
|
'&:first-child': {
|
||||||
|
marginLeft: 0,
|
||||||
|
},
|
||||||
|
'button&': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
'200': {
|
||||||
|
marginLeft: toRem(-6),
|
||||||
|
outlineWidth: config.borderWidth.B300,
|
||||||
|
},
|
||||||
|
'300': {
|
||||||
|
marginLeft: toRem(-9),
|
||||||
|
outlineWidth: config.borderWidth.B400,
|
||||||
|
},
|
||||||
|
'400': {
|
||||||
|
marginLeft: toRem(-10.5),
|
||||||
|
outlineWidth: config.borderWidth.B500,
|
||||||
|
},
|
||||||
|
'500': {
|
||||||
|
marginLeft: toRem(-13),
|
||||||
|
outlineWidth: config.borderWidth.B600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
Background: getVariant('Background'),
|
||||||
|
Surface: getVariant('Surface'),
|
||||||
|
SurfaceVariant: getVariant('SurfaceVariant'),
|
||||||
|
Primary: getVariant('Primary'),
|
||||||
|
Secondary: getVariant('Secondary'),
|
||||||
|
Success: getVariant('Success'),
|
||||||
|
Warning: getVariant('Warning'),
|
||||||
|
Critical: getVariant('Critical'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: '400',
|
||||||
|
variant: 'Surface',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type StackedAvatarVariants = RecipeVariants<typeof StackedAvatar>;
|
||||||
@@ -26,7 +26,12 @@ export function SSOStage({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMessage = (evt: MessageEvent) => {
|
const handleMessage = (evt: MessageEvent) => {
|
||||||
if (ssoWindow && evt.data === 'authDone' && evt.source === ssoWindow) {
|
if (
|
||||||
|
evt.origin === new URL(ssoRedirectURL).origin &&
|
||||||
|
ssoWindow &&
|
||||||
|
evt.data === 'authDone' &&
|
||||||
|
evt.source === ssoWindow
|
||||||
|
) {
|
||||||
ssoWindow.close();
|
ssoWindow.close();
|
||||||
setSSOWindow(undefined);
|
setSSOWindow(undefined);
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
@@ -37,7 +42,7 @@ export function SSOStage({
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('message', handleMessage);
|
window.removeEventListener('message', handleMessage);
|
||||||
};
|
};
|
||||||
}, [ssoWindow, handleSubmit]);
|
}, [ssoWindow, handleSubmit, ssoRedirectURL]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export const UrlPreviewImg = style([
|
|||||||
objectPosition: 'center',
|
objectPosition: 'center',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
cursor: 'pointer',
|
||||||
|
|
||||||
|
':hover': {
|
||||||
|
filter: 'brightness(0.8)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { IPreviewUrlResponse } from 'matrix-js-sdk';
|
import { IPreviewUrlResponse } from 'matrix-js-sdk';
|
||||||
import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds';
|
import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds';
|
||||||
|
import { ImageOverlay } from '../ImageOverlay';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview';
|
import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview';
|
||||||
@@ -12,6 +13,8 @@ import * as css from './UrlPreviewCard.css';
|
|||||||
import { tryDecodeURIComponent } from '../../utils/dom';
|
import { tryDecodeURIComponent } from '../../utils/dom';
|
||||||
import { mxcUrlToHttp } from '../../utils/matrix';
|
import { mxcUrlToHttp } from '../../utils/matrix';
|
||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
|
import { ImageViewer } from '../image-viewer';
|
||||||
|
import { onEnterOrSpace } from '../../utils/keyboard';
|
||||||
|
|
||||||
const linkStyles = { color: color.Success.Main };
|
const linkStyles = { color: color.Success.Main };
|
||||||
|
|
||||||
@@ -19,6 +22,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
|||||||
({ url, ts, ...props }, ref) => {
|
({ url, ts, ...props }, ref) => {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
|
const [viewer, setViewer] = useState(false);
|
||||||
const [previewStatus, loadPreview] = useAsyncCallback(
|
const [previewStatus, loadPreview] = useAsyncCallback(
|
||||||
useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
|
useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
|
||||||
);
|
);
|
||||||
@@ -30,11 +34,41 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
|||||||
if (previewStatus.status === AsyncStatus.Error) return null;
|
if (previewStatus.status === AsyncStatus.Error) return null;
|
||||||
|
|
||||||
const renderContent = (prev: IPreviewUrlResponse) => {
|
const renderContent = (prev: IPreviewUrlResponse) => {
|
||||||
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication, 256, 256, 'scale', false);
|
const thumbUrl = mxcUrlToHttp(
|
||||||
|
mx,
|
||||||
|
prev['og:image'] || '',
|
||||||
|
useAuthentication,
|
||||||
|
256,
|
||||||
|
256,
|
||||||
|
'scale',
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{imgUrl && <UrlPreviewImg src={imgUrl} alt={prev['og:title']} title={prev['og:title']} />}
|
{thumbUrl && (
|
||||||
|
<UrlPreviewImg
|
||||||
|
src={thumbUrl}
|
||||||
|
alt={prev['og:title']}
|
||||||
|
title={prev['og:title']}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(evt) => onEnterOrSpace(() => setViewer(true))(evt)}
|
||||||
|
onClick={() => setViewer(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{imgUrl && (
|
||||||
|
<ImageOverlay
|
||||||
|
src={imgUrl}
|
||||||
|
alt={prev['og:title']}
|
||||||
|
viewer={viewer}
|
||||||
|
requestClose={() => {
|
||||||
|
setViewer(false);
|
||||||
|
}}
|
||||||
|
renderViewer={(p) => <ImageViewer {...p} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<UrlPreviewContent>
|
<UrlPreviewContent>
|
||||||
<Text
|
<Text
|
||||||
style={linkStyles}
|
style={linkStyles}
|
||||||
@@ -42,7 +76,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
|||||||
as="a"
|
as="a"
|
||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="no-referrer"
|
rel="noreferrer"
|
||||||
size="T200"
|
size="T200"
|
||||||
priority="300"
|
priority="300"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RoomIcon size="100" joinRule={room.getJoinRule()} />
|
<RoomIcon size="100" joinRule={room.getJoinRule()} roomType={room.getType()} />
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ export type AutoDiscoveryInfo = Record<string, unknown> & {
|
|||||||
'm.identity_server'?: {
|
'm.identity_server'?: {
|
||||||
base_url: string;
|
base_url: string;
|
||||||
};
|
};
|
||||||
|
'org.matrix.msc2965.authentication'?: {
|
||||||
|
account?: string;
|
||||||
|
issuer?: string;
|
||||||
|
};
|
||||||
|
'org.matrix.msc4143.rtc_foci'?: [
|
||||||
|
{
|
||||||
|
livekit_service_url: string;
|
||||||
|
type: 'livekit';
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const autoDiscovery = async (
|
export const autoDiscovery = async (
|
||||||
|
|||||||
@@ -291,7 +291,11 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RoomIcon size="200" joinRule={room.getJoinRule()} />
|
<RoomIcon
|
||||||
|
size="200"
|
||||||
|
joinRule={room.getJoinRule()}
|
||||||
|
roomType={room.getType()}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
|
|||||||
226
src/app/features/call-status/CallControl.tsx
Normal file
226
src/app/features/call-status/CallControl.tsx
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import { Box, Chip, Icon, IconButton, Icons, Spinner, Text, Tooltip, TooltipProvider } from 'folds';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { StatusDivider } from './components';
|
||||||
|
import { CallEmbed, useCallControlState } from '../../plugins/call';
|
||||||
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
import { callEmbedAtom } from '../../state/callEmbed';
|
||||||
|
|
||||||
|
type MicrophoneButtonProps = {
|
||||||
|
enabled: boolean;
|
||||||
|
onToggle: () => Promise<unknown>;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={anchorRef}
|
||||||
|
variant={enabled ? 'Surface' : 'Warning'}
|
||||||
|
fill="Soft"
|
||||||
|
radii="300"
|
||||||
|
size="300"
|
||||||
|
onClick={() => onToggle()}
|
||||||
|
outlined
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<Icon size="100" src={enabled ? Icons.Mic : Icons.MicMute} filled={!enabled} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SoundButtonProps = {
|
||||||
|
enabled: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
function SoundButton({ enabled, onToggle, disabled }: SoundButtonProps) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{enabled ? 'Turn Off Sound' : 'Turn On Sound'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={anchorRef}
|
||||||
|
variant={enabled ? 'Surface' : 'Warning'}
|
||||||
|
fill="Soft"
|
||||||
|
radii="300"
|
||||||
|
size="300"
|
||||||
|
onClick={() => onToggle()}
|
||||||
|
outlined
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
size="100"
|
||||||
|
src={enabled ? Icons.Headphone : Icons.HeadphoneMute}
|
||||||
|
filled={!enabled}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type VideoButtonProps = {
|
||||||
|
enabled: boolean;
|
||||||
|
onToggle: () => Promise<unknown>;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{enabled ? 'Stop Camera' : 'Start Camera'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={anchorRef}
|
||||||
|
variant={enabled ? 'Success' : 'Surface'}
|
||||||
|
fill="Soft"
|
||||||
|
radii="300"
|
||||||
|
size="300"
|
||||||
|
onClick={() => onToggle()}
|
||||||
|
outlined
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
size="100"
|
||||||
|
src={enabled ? Icons.VideoCamera : Icons.VideoCameraMute}
|
||||||
|
filled={enabled}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScreenShareButtonProps = {
|
||||||
|
enabled: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
function ScreenShareButton({ enabled, onToggle, disabled }: ScreenShareButtonProps) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{enabled ? 'Stop Screenshare' : 'Start Screenshare'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={anchorRef}
|
||||||
|
variant={enabled ? 'Success' : 'Surface'}
|
||||||
|
fill="Soft"
|
||||||
|
radii="300"
|
||||||
|
size="300"
|
||||||
|
onClick={onToggle}
|
||||||
|
outlined
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<Icon size="100" src={Icons.ScreenShare} filled={enabled} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CallControl({
|
||||||
|
callEmbed,
|
||||||
|
compact,
|
||||||
|
callJoined,
|
||||||
|
}: {
|
||||||
|
callEmbed: CallEmbed;
|
||||||
|
compact: boolean;
|
||||||
|
callJoined: boolean;
|
||||||
|
}) {
|
||||||
|
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
|
||||||
|
const setCallEmbed = useSetAtom(callEmbedAtom);
|
||||||
|
|
||||||
|
const [hangupState, hangup] = useAsyncCallback(
|
||||||
|
useCallback(() => callEmbed.hangup(), [callEmbed])
|
||||||
|
);
|
||||||
|
const exiting =
|
||||||
|
hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success;
|
||||||
|
|
||||||
|
const handleHangup = () => {
|
||||||
|
if (!callJoined) {
|
||||||
|
setCallEmbed(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hangup();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box shrink="No" alignItems="Center" gap="300">
|
||||||
|
<Box alignItems="Inherit" gap="200">
|
||||||
|
<MicrophoneButton
|
||||||
|
enabled={microphone}
|
||||||
|
onToggle={() => callEmbed.control.toggleMicrophone()}
|
||||||
|
disabled={!callJoined}
|
||||||
|
/>
|
||||||
|
<SoundButton
|
||||||
|
enabled={sound}
|
||||||
|
onToggle={() => callEmbed.control.toggleSound()}
|
||||||
|
disabled={!callJoined}
|
||||||
|
/>
|
||||||
|
{!compact && <StatusDivider />}
|
||||||
|
<VideoButton
|
||||||
|
enabled={video}
|
||||||
|
onToggle={() => callEmbed.control.toggleVideo()}
|
||||||
|
disabled={!callJoined}
|
||||||
|
/>
|
||||||
|
{!compact && (
|
||||||
|
<ScreenShareButton
|
||||||
|
enabled={screenshare}
|
||||||
|
onToggle={() => callEmbed.control.toggleScreenshare()}
|
||||||
|
disabled={!callJoined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<StatusDivider />
|
||||||
|
<Chip
|
||||||
|
variant="Critical"
|
||||||
|
radii="Pill"
|
||||||
|
fill="Soft"
|
||||||
|
before={
|
||||||
|
exiting ? (
|
||||||
|
<Spinner variant="Critical" fill="Soft" size="50" />
|
||||||
|
) : (
|
||||||
|
<Icon size="50" src={Icons.PhoneDown} filled />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={exiting}
|
||||||
|
outlined
|
||||||
|
onClick={handleHangup}
|
||||||
|
>
|
||||||
|
{!compact && (
|
||||||
|
<Text as="span" size="L400">
|
||||||
|
End
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Chip>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
src/app/features/call-status/CallRoomName.tsx
Normal file
55
src/app/features/call-status/CallRoomName.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Room } from 'matrix-js-sdk';
|
||||||
|
import { Chip, Text } from 'folds';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useRoomName } from '../../hooks/useRoomMeta';
|
||||||
|
import { RoomIcon } from '../../components/room-avatar';
|
||||||
|
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||||
|
import { getAllParents, guessPerfectParent } from '../../utils/room';
|
||||||
|
import { useOrphanSpaces } from '../../state/hooks/roomList';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import { allRoomsAtom } from '../../state/room-list/roomList';
|
||||||
|
import { mDirectAtom } from '../../state/mDirectList';
|
||||||
|
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
|
||||||
|
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||||
|
|
||||||
|
type CallRoomNameProps = {
|
||||||
|
room: Room;
|
||||||
|
};
|
||||||
|
export function CallRoomName({ room }: CallRoomNameProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const name = useRoomName(room);
|
||||||
|
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||||
|
const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, roomToParents);
|
||||||
|
const mDirects = useAtomValue(mDirectAtom);
|
||||||
|
const dm = mDirects.has(room.roomId);
|
||||||
|
|
||||||
|
const allRoomsSet = useAllJoinedRoomsSet();
|
||||||
|
const getRoom = useGetRoom(allRoomsSet);
|
||||||
|
|
||||||
|
const allParents = getAllParents(roomToParents, room.roomId);
|
||||||
|
const orphanParents = allParents && orphanSpaces.filter((o) => allParents.has(o));
|
||||||
|
const perfectOrphanParent = orphanParents && guessPerfectParent(mx, room.roomId, orphanParents);
|
||||||
|
|
||||||
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
variant="Background"
|
||||||
|
radii="Pill"
|
||||||
|
before={
|
||||||
|
<RoomIcon size="200" joinRule={room.getJoinRule()} roomType={room.getType()} filled />
|
||||||
|
}
|
||||||
|
onClick={() => navigateRoom(room.roomId)}
|
||||||
|
>
|
||||||
|
<Text size="L400" truncate>
|
||||||
|
{name}
|
||||||
|
{!dm && perfectOrphanParent && (
|
||||||
|
<Text as="span" size="T200" priority="300">
|
||||||
|
{' •'} <b>{getRoom(perfectOrphanParent)?.name ?? perfectOrphanParent}</b>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Chip>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
src/app/features/call-status/CallStatus.tsx
Normal file
81
src/app/features/call-status/CallStatus.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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(room, callSession);
|
||||||
|
const screenSize = useScreenSize();
|
||||||
|
const callJoined = useCallJoined(callEmbed);
|
||||||
|
const speakers = useCallSpeakers(callEmbed);
|
||||||
|
|
||||||
|
const compact = screenSize === ScreenSize.Mobile;
|
||||||
|
|
||||||
|
const memberVisible = callJoined && callMembers.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={classNames(css.CallStatus, ContainerColor({ variant: 'Background' }))}
|
||||||
|
shrink="No"
|
||||||
|
gap="400"
|
||||||
|
alignItems={compact ? undefined : 'Center'}
|
||||||
|
direction={compact ? 'Column' : 'Row'}
|
||||||
|
>
|
||||||
|
<Box grow="Yes" alignItems="Center" gap="200">
|
||||||
|
{memberVisible ? (
|
||||||
|
<Box shrink="No">
|
||||||
|
<LiveChip count={callMembers.length} room={room} members={callMembers} />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Spinner variant="Secondary" size="200" />
|
||||||
|
)}
|
||||||
|
<Box grow="Yes" alignItems="Center" gap="Inherit">
|
||||||
|
{!compact && (
|
||||||
|
<>
|
||||||
|
<CallRoomName room={room} />
|
||||||
|
{speakers.size > 0 && (
|
||||||
|
<>
|
||||||
|
<StatusDivider />
|
||||||
|
<span data-spacing-node />
|
||||||
|
<MemberSpeaking room={room} speakers={speakers} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{memberVisible && (
|
||||||
|
<Box shrink="No">
|
||||||
|
<MemberGlance room={room} members={callMembers} speakers={speakers} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{memberVisible && !compact && <StatusDivider />}
|
||||||
|
<Box shrink="No" alignItems="Center" gap="Inherit">
|
||||||
|
{compact && (
|
||||||
|
<Box grow="Yes">
|
||||||
|
<CallRoomName room={room} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<CallControl callJoined={callJoined} compact={compact} callEmbed={callEmbed} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
137
src/app/features/call-status/LiveChip.tsx
Normal file
137
src/app/features/call-status/LiveChip.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import React, { MouseEventHandler, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Chip,
|
||||||
|
config,
|
||||||
|
Icon,
|
||||||
|
Icons,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
PopOut,
|
||||||
|
RectCords,
|
||||||
|
Scroll,
|
||||||
|
Text,
|
||||||
|
toRem,
|
||||||
|
} from 'folds';
|
||||||
|
import { CallMembership } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import { Room } from 'matrix-js-sdk';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
|
||||||
|
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
|
import { UserAvatar } from '../../components/user-avatar';
|
||||||
|
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
|
||||||
|
import { getMouseEventCords } from '../../utils/dom';
|
||||||
|
|
||||||
|
type LiveChipProps = {
|
||||||
|
room: Room;
|
||||||
|
members: CallMembership[];
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
export function LiveChip({ count, room, members }: LiveChipProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const useAuthentication = useMediaAuthentication();
|
||||||
|
const openUserProfile = useOpenUserRoomProfile();
|
||||||
|
|
||||||
|
const [cords, setCords] = useState<RectCords>();
|
||||||
|
|
||||||
|
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
|
setCords(evt.currentTarget.getBoundingClientRect());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopOut
|
||||||
|
anchor={cords}
|
||||||
|
position="Top"
|
||||||
|
align="Start"
|
||||||
|
content={
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => setCords(undefined),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu
|
||||||
|
style={{
|
||||||
|
maxHeight: '75vh',
|
||||||
|
maxWidth: toRem(300),
|
||||||
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Scroll size="0" hideTrack visibility="Hover">
|
||||||
|
<Box direction="Column" style={{ padding: config.space.S100 }}>
|
||||||
|
{members.map((callMember) => {
|
||||||
|
const userId = callMember.sender;
|
||||||
|
if (!userId) return null;
|
||||||
|
const name =
|
||||||
|
getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
|
||||||
|
const avatarMxc = getMemberAvatarMxc(room, userId);
|
||||||
|
const avatarUrl = avatarMxc
|
||||||
|
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96) ?? undefined
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={callMember.membershipID}
|
||||||
|
size="400"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
style={{ paddingLeft: config.space.S200 }}
|
||||||
|
onClick={(evt) =>
|
||||||
|
openUserProfile(
|
||||||
|
room.roomId,
|
||||||
|
undefined,
|
||||||
|
userId,
|
||||||
|
getMouseEventCords(evt.nativeEvent),
|
||||||
|
'Right'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
before={
|
||||||
|
<Avatar size="200" radii="400">
|
||||||
|
<UserAvatar
|
||||||
|
userId={userId}
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={name}
|
||||||
|
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
||||||
|
/>
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Scroll>
|
||||||
|
</Box>
|
||||||
|
</Menu>
|
||||||
|
</FocusTrap>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
variant="Surface"
|
||||||
|
fill="Soft"
|
||||||
|
before={<Badge variant="Critical" fill="Solid" size="200" />}
|
||||||
|
after={<Icon size="50" src={cords ? Icons.ChevronBottom : Icons.ChevronTop} />}
|
||||||
|
radii="Pill"
|
||||||
|
onClick={handleOpenMenu}
|
||||||
|
>
|
||||||
|
<Text className={css.LiveChipText} as="span" size="L400" truncate>
|
||||||
|
{count} Live
|
||||||
|
</Text>
|
||||||
|
</Chip>
|
||||||
|
</PopOut>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
src/app/features/call-status/MemberGlance.tsx
Normal file
75
src/app/features/call-status/MemberGlance.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Box, config, Icon, Icons, Text } from 'folds';
|
||||||
|
import { CallMembership } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
|
||||||
|
import React from 'react';
|
||||||
|
import { Room } from 'matrix-js-sdk';
|
||||||
|
import { UserAvatar } from '../../components/user-avatar';
|
||||||
|
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
|
||||||
|
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
|
import { StackedAvatar } from '../../components/stacked-avatar';
|
||||||
|
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
|
||||||
|
import { getMouseEventCords } from '../../utils/dom';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
|
||||||
|
type MemberGlanceProps = {
|
||||||
|
room: Room;
|
||||||
|
members: CallMembership[];
|
||||||
|
speakers: Set<string>;
|
||||||
|
max?: number;
|
||||||
|
};
|
||||||
|
export function MemberGlance({ room, members, speakers, max = 6 }: MemberGlanceProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const useAuthentication = useMediaAuthentication();
|
||||||
|
const openUserProfile = useOpenUserRoomProfile();
|
||||||
|
|
||||||
|
const visibleMembers = members.slice(0, max);
|
||||||
|
const remainingCount = max && members.length > max ? members.length - max : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box alignItems="Center">
|
||||||
|
{visibleMembers.map((callMember) => {
|
||||||
|
const userId = callMember.sender;
|
||||||
|
if (!userId) return null;
|
||||||
|
const name = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
|
||||||
|
const avatarMxc = getMemberAvatarMxc(room, userId);
|
||||||
|
const avatarUrl = avatarMxc
|
||||||
|
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96) ?? undefined
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StackedAvatar
|
||||||
|
key={callMember.membershipID}
|
||||||
|
className={speakers.has(callMember.sender) ? css.SpeakerAvatarOutline : undefined}
|
||||||
|
title={name}
|
||||||
|
as="button"
|
||||||
|
variant="Background"
|
||||||
|
size="200"
|
||||||
|
radii="Pill"
|
||||||
|
onClick={(evt) =>
|
||||||
|
openUserProfile(
|
||||||
|
room.roomId,
|
||||||
|
undefined,
|
||||||
|
userId,
|
||||||
|
getMouseEventCords(evt.nativeEvent),
|
||||||
|
'Top'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<UserAvatar
|
||||||
|
userId={userId}
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={name}
|
||||||
|
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
||||||
|
/>
|
||||||
|
</StackedAvatar>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{remainingCount > 0 && (
|
||||||
|
<Text size="L400" style={{ paddingLeft: config.space.S100 }}>
|
||||||
|
+{remainingCount}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
src/app/features/call-status/MemberSpeaking.tsx
Normal file
78
src/app/features/call-status/MemberSpeaking.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { Room } from 'matrix-js-sdk';
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Icon, Icons, Text } from 'folds';
|
||||||
|
import { getMemberDisplayName } from '../../utils/room';
|
||||||
|
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||||
|
|
||||||
|
type MemberSpeakingProps = {
|
||||||
|
room: Room;
|
||||||
|
speakers: Set<string>;
|
||||||
|
};
|
||||||
|
export function MemberSpeaking({ room, speakers }: MemberSpeakingProps) {
|
||||||
|
const speakingNames = Array.from(speakers).map(
|
||||||
|
(userId) => getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Box alignItems="Center" gap="100">
|
||||||
|
<Icon size="100" src={Icons.Mic} filled />
|
||||||
|
<Text size="T200" truncate>
|
||||||
|
{speakingNames.length === 1 && (
|
||||||
|
<>
|
||||||
|
<b>{speakingNames[0]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' is speaking...'}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{speakingNames.length === 2 && (
|
||||||
|
<>
|
||||||
|
<b>{speakingNames[0]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' and '}
|
||||||
|
</Text>
|
||||||
|
<b>{speakingNames[1]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' are speaking...'}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{speakingNames.length === 3 && (
|
||||||
|
<>
|
||||||
|
<b>{speakingNames[0]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{', '}
|
||||||
|
</Text>
|
||||||
|
<b>{speakingNames[1]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' and '}
|
||||||
|
</Text>
|
||||||
|
<b>{speakingNames[2]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' are speaking...'}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{speakingNames.length > 3 && (
|
||||||
|
<>
|
||||||
|
<b>{speakingNames[0]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{', '}
|
||||||
|
</Text>
|
||||||
|
<b>{speakingNames[1]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{', '}
|
||||||
|
</Text>
|
||||||
|
<b>{speakingNames[2]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' and '}
|
||||||
|
</Text>
|
||||||
|
<b>{speakingNames.length - 3} others</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' are speaking...'}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
src/app/features/call-status/components.tsx
Normal file
9
src/app/features/call-status/components.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Line } from 'folds';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
|
||||||
|
export function StatusDivider() {
|
||||||
|
return (
|
||||||
|
<Line variant="Background" size="300" direction="Vertical" className={css.ControlDivider} />
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/app/features/call-status/index.ts
Normal file
1
src/app/features/call-status/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './CallStatus';
|
||||||
21
src/app/features/call-status/styles.css.ts
Normal file
21
src/app/features/call-status/styles.css.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
import { color, config, toRem } from 'folds';
|
||||||
|
|
||||||
|
export const LiveChipText = style({
|
||||||
|
color: color.Critical.Main,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CallStatus = style([
|
||||||
|
{
|
||||||
|
padding: `${toRem(6)} ${config.space.S200}`,
|
||||||
|
borderTop: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ControlDivider = style({
|
||||||
|
height: toRem(16),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SpeakerAvatarOutline = style({
|
||||||
|
boxShadow: `0 0 0 ${config.borderWidth.B600} ${color.Success.Main}`,
|
||||||
|
});
|
||||||
203
src/app/features/call/CallControls.tsx
Normal file
203
src/app/features/call/CallControls.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import React, { MouseEventHandler, useCallback, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
config,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Icons,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
PopOut,
|
||||||
|
RectCords,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
toRem,
|
||||||
|
} from 'folds';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import { SequenceCard } from '../../components/sequence-card';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
import {
|
||||||
|
ChatButton,
|
||||||
|
ControlDivider,
|
||||||
|
MicrophoneButton,
|
||||||
|
ScreenShareButton,
|
||||||
|
SoundButton,
|
||||||
|
VideoButton,
|
||||||
|
} from './Controls';
|
||||||
|
import { CallEmbed, useCallControlState } from '../../plugins/call';
|
||||||
|
import { useResizeObserver } from '../../hooks/useResizeObserver';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
|
||||||
|
type CallControlsProps = {
|
||||||
|
callEmbed: CallEmbed;
|
||||||
|
};
|
||||||
|
export function CallControls({ callEmbed }: CallControlsProps) {
|
||||||
|
const controlRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [compact, setCompact] = useState(document.body.clientWidth < 500);
|
||||||
|
|
||||||
|
useResizeObserver(
|
||||||
|
useCallback(() => {
|
||||||
|
const element = controlRef.current;
|
||||||
|
if (!element) return;
|
||||||
|
setCompact(element.clientWidth < 500);
|
||||||
|
}, []),
|
||||||
|
useCallback(() => controlRef.current, [])
|
||||||
|
);
|
||||||
|
|
||||||
|
const { microphone, video, sound, screenshare, spotlight } = useCallControlState(
|
||||||
|
callEmbed.control
|
||||||
|
);
|
||||||
|
|
||||||
|
const [cords, setCords] = useState<RectCords>();
|
||||||
|
|
||||||
|
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
|
setCords(evt.currentTarget.getBoundingClientRect());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSpotlightClick = () => {
|
||||||
|
callEmbed.control.toggleSpotlight();
|
||||||
|
setCords(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReactionsClick = () => {
|
||||||
|
callEmbed.control.toggleReactions();
|
||||||
|
setCords(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettingsClick = () => {
|
||||||
|
callEmbed.control.toggleSettings();
|
||||||
|
setCords(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [hangupState, hangup] = useAsyncCallback(
|
||||||
|
useCallback(() => callEmbed.hangup(), [callEmbed])
|
||||||
|
);
|
||||||
|
const exiting =
|
||||||
|
hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={controlRef}
|
||||||
|
className={css.CallControlContainer}
|
||||||
|
justifyContent="Center"
|
||||||
|
alignItems="Center"
|
||||||
|
>
|
||||||
|
<SequenceCard
|
||||||
|
className={css.ControlCard}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
gap="400"
|
||||||
|
radii="500"
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="SpaceBetween"
|
||||||
|
>
|
||||||
|
<Box alignItems="Center" gap="Inherit" grow="Yes" direction={compact ? 'Column' : 'Row'}>
|
||||||
|
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
||||||
|
<MicrophoneButton
|
||||||
|
enabled={microphone}
|
||||||
|
onToggle={() => callEmbed.control.toggleMicrophone()}
|
||||||
|
/>
|
||||||
|
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
|
||||||
|
</Box>
|
||||||
|
{!compact && <ControlDivider />}
|
||||||
|
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
||||||
|
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
|
||||||
|
<ScreenShareButton
|
||||||
|
enabled={screenshare}
|
||||||
|
onToggle={() => callEmbed.control.toggleScreenshare()}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{!compact && <ControlDivider />}
|
||||||
|
<Box alignItems="Center" gap="Inherit" grow="Yes" direction={compact ? 'Column' : 'Row'}>
|
||||||
|
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
||||||
|
<ChatButton />
|
||||||
|
<PopOut
|
||||||
|
anchor={cords}
|
||||||
|
position="Top"
|
||||||
|
align="Center"
|
||||||
|
content={
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => setCords(undefined),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu>
|
||||||
|
<Box direction="Column" style={{ padding: config.space.S100 }}>
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
onClick={handleSpotlightClick}
|
||||||
|
>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
{spotlight ? 'Grid View' : 'Spotlight View'}
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
onClick={handleReactionsClick}
|
||||||
|
>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
Reactions
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
onClick={handleSettingsClick}
|
||||||
|
>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
Settings
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
</Box>
|
||||||
|
</Menu>
|
||||||
|
</FocusTrap>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
variant="Surface"
|
||||||
|
fill="Soft"
|
||||||
|
radii="400"
|
||||||
|
size="400"
|
||||||
|
onClick={handleOpenMenu}
|
||||||
|
outlined
|
||||||
|
aria-pressed={!!cords}
|
||||||
|
>
|
||||||
|
<Icon size="400" src={Icons.VerticalDots} />
|
||||||
|
</IconButton>
|
||||||
|
</PopOut>
|
||||||
|
</Box>
|
||||||
|
<Box shrink="No" direction="Column">
|
||||||
|
<Button
|
||||||
|
style={{ minWidth: toRem(88) }}
|
||||||
|
variant="Critical"
|
||||||
|
fill="Solid"
|
||||||
|
onClick={hangup}
|
||||||
|
before={
|
||||||
|
exiting ? (
|
||||||
|
<Spinner variant="Critical" fill="Solid" size="200" />
|
||||||
|
) : (
|
||||||
|
<Icon src={Icons.PhoneDown} size="200" filled />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={exiting}
|
||||||
|
>
|
||||||
|
<Text size="B400">End</Text>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</SequenceCard>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
src/app/features/call/CallMemberCard.tsx
Normal file
121
src/app/features/call/CallMemberCard.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { CallMembership, SessionMembershipData } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Avatar, Box, Icon, Icons, Text } from 'folds';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
|
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
|
||||||
|
import { SequenceCard } from '../../components/sequence-card';
|
||||||
|
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
|
||||||
|
import { useRoom } from '../../hooks/useRoom';
|
||||||
|
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
|
||||||
|
import { UserAvatar } from '../../components/user-avatar';
|
||||||
|
import { getMouseEventCords } from '../../utils/dom';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
|
||||||
|
interface MemberWithMembershipData {
|
||||||
|
membershipData?: SessionMembershipData & {
|
||||||
|
'm.call.intent': 'video' | 'audio';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallMemberCardProps = {
|
||||||
|
member: CallMembership;
|
||||||
|
};
|
||||||
|
export function CallMemberCard({ member }: CallMemberCardProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const useAuthentication = useMediaAuthentication();
|
||||||
|
const room = useRoom();
|
||||||
|
|
||||||
|
const openUserProfile = useOpenUserRoomProfile();
|
||||||
|
|
||||||
|
const userId = member.sender;
|
||||||
|
if (!userId) return null;
|
||||||
|
|
||||||
|
const name = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
|
||||||
|
const avatarMxc = getMemberAvatarMxc(room, userId);
|
||||||
|
const avatarUrl = avatarMxc
|
||||||
|
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96) ?? undefined
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const audioOnly =
|
||||||
|
(member as unknown as MemberWithMembershipData).membershipData?.['m.call.intent'] === 'audio';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SequenceCard
|
||||||
|
as="button"
|
||||||
|
key={member.membershipID}
|
||||||
|
className={css.CallMemberCard}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
radii="500"
|
||||||
|
onClick={(evt: any) =>
|
||||||
|
openUserProfile(
|
||||||
|
room.roomId,
|
||||||
|
undefined,
|
||||||
|
userId,
|
||||||
|
getMouseEventCords(evt.nativeEvent),
|
||||||
|
'Right'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box grow="Yes" gap="300" alignItems="Center">
|
||||||
|
<Avatar size="200" radii="400">
|
||||||
|
<UserAvatar
|
||||||
|
userId={userId}
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={name}
|
||||||
|
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
||||||
|
/>
|
||||||
|
</Avatar>
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Text size="L400" truncate>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
{audioOnly && <Icon src={Icons.VideoCameraMute} size="100" />}
|
||||||
|
</Box>
|
||||||
|
</SequenceCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CallMemberRenderer({
|
||||||
|
members,
|
||||||
|
max = 4,
|
||||||
|
}: {
|
||||||
|
members: CallMembership[];
|
||||||
|
max?: number;
|
||||||
|
}) {
|
||||||
|
const [viewMore, setViewMore] = useState(false);
|
||||||
|
|
||||||
|
const truncatedMembers = viewMore ? members : members.slice(0, 4);
|
||||||
|
const remaining = members.length - truncatedMembers.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{truncatedMembers.map((member) => (
|
||||||
|
<CallMemberCard key={member.membershipID} member={member} />
|
||||||
|
))}
|
||||||
|
{members.length > max && (
|
||||||
|
<SequenceCard
|
||||||
|
as="button"
|
||||||
|
className={css.CallMemberCard}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
radii="500"
|
||||||
|
onClick={() => setViewMore(!viewMore)}
|
||||||
|
>
|
||||||
|
<Box grow="Yes" gap="300" alignItems="Center">
|
||||||
|
{viewMore ? (
|
||||||
|
<Text size="L400" truncate>
|
||||||
|
Collapse
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text size="L400" truncate>
|
||||||
|
{remaining === 0 ? `+${remaining} Other` : `+${remaining} Others`}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Icon src={viewMore ? Icons.ChevronTop : Icons.ChevronBottom} size="100" />
|
||||||
|
</SequenceCard>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
150
src/app/features/call/CallView.tsx
Normal file
150
src/app/features/call/CallView.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
function LivekitServerMissingMessage() {
|
||||||
|
return (
|
||||||
|
<Text style={{ margin: 'auto', color: color.Critical.Main }} size="L400" align="Center">
|
||||||
|
Your homeserver does not support calling. But you can still join call started by others.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function JoinMessage({
|
||||||
|
hasParticipant,
|
||||||
|
livekitSupported,
|
||||||
|
}: {
|
||||||
|
hasParticipant?: boolean;
|
||||||
|
livekitSupported?: boolean;
|
||||||
|
}) {
|
||||||
|
if (hasParticipant) return null;
|
||||||
|
|
||||||
|
if (livekitSupported === false) {
|
||||||
|
return <LivekitServerMissingMessage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text style={{ margin: 'auto' }} size="L400" align="Center">
|
||||||
|
Voice chat’s empty — Be the first to hop in!
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NoPermissionMessage() {
|
||||||
|
return (
|
||||||
|
<Text style={{ margin: 'auto' }} size="L400" align="Center">
|
||||||
|
You don't have permission to join!
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlreadyInCallMessage() {
|
||||||
|
return (
|
||||||
|
<Text style={{ margin: 'auto', color: color.Warning.Main }} size="L400" align="Center">
|
||||||
|
Already in another call — End the current call to join!
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CallPrescreen() {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const room = useRoom();
|
||||||
|
const livekitSupported = useLivekitSupport();
|
||||||
|
|
||||||
|
const powerLevels = usePowerLevelsContext();
|
||||||
|
const creators = useRoomCreators(room);
|
||||||
|
|
||||||
|
const permissions = useRoomPermissions(creators, powerLevels);
|
||||||
|
const hasPermission = permissions.event(StateEvent.GroupCallMemberPrefix, mx.getSafeUserId());
|
||||||
|
|
||||||
|
const callSession = useCallSession(room);
|
||||||
|
const callMembers = useCallMembers(room, callSession);
|
||||||
|
const hasParticipant = callMembers.length > 0;
|
||||||
|
|
||||||
|
const callEmbed = useCallEmbed();
|
||||||
|
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
|
||||||
|
|
||||||
|
const canJoin = hasPermission && (livekitSupported || hasParticipant);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Scroll variant="Surface" hideTrack>
|
||||||
|
<Box className={css.CallViewContent} alignItems="Center" justifyContent="Center">
|
||||||
|
<Box style={{ maxWidth: toRem(382), width: '100%' }} direction="Column" gap="100">
|
||||||
|
{hasParticipant && (
|
||||||
|
<Header size="300">
|
||||||
|
<Box grow="Yes" alignItems="Center">
|
||||||
|
<Text size="L400">Participant</Text>
|
||||||
|
</Box>
|
||||||
|
<Badge variant="Critical" fill="Solid" size="400">
|
||||||
|
<Text as="span" size="L400" truncate>
|
||||||
|
{callMembers.length} Live
|
||||||
|
</Text>
|
||||||
|
</Badge>
|
||||||
|
</Header>
|
||||||
|
)}
|
||||||
|
<CallMemberRenderer members={callMembers} />
|
||||||
|
<PrescreenControls canJoin={canJoin} />
|
||||||
|
<Box className={css.PrescreenMessage} alignItems="Center">
|
||||||
|
{!inOtherCall &&
|
||||||
|
(hasPermission ? (
|
||||||
|
<JoinMessage hasParticipant={hasParticipant} livekitSupported={livekitSupported} />
|
||||||
|
) : (
|
||||||
|
<NoPermissionMessage />
|
||||||
|
))}
|
||||||
|
{inOtherCall && <AlreadyInCallMessage />}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Scroll>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallJoinedProps = {
|
||||||
|
containerRef: RefObject<HTMLDivElement>;
|
||||||
|
joined: boolean;
|
||||||
|
};
|
||||||
|
function CallJoined({ joined, containerRef }: CallJoinedProps) {
|
||||||
|
const callEmbed = useCallEmbed();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box grow="Yes" direction="Column">
|
||||||
|
<Box grow="Yes" ref={containerRef} />
|
||||||
|
{callEmbed && joined && <CallControls callEmbed={callEmbed} />}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CallView() {
|
||||||
|
const room = useRoom();
|
||||||
|
const callContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
useCallEmbedPlacementSync(callContainerRef);
|
||||||
|
|
||||||
|
const callEmbed = useCallEmbed();
|
||||||
|
const callJoined = useCallJoined(callEmbed);
|
||||||
|
|
||||||
|
const currentJoined = callEmbed?.roomId === room.roomId && callJoined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={ContainerColor({ variant: 'Surface' })}
|
||||||
|
style={{ minWidth: toRem(280) }}
|
||||||
|
grow="Yes"
|
||||||
|
>
|
||||||
|
{!currentJoined && <CallPrescreen />}
|
||||||
|
<CallJoined joined={currentJoined} containerRef={callContainerRef} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
177
src/app/features/call/Controls.tsx
Normal file
177
src/app/features/call/Controls.tsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Icon, IconButton, Icons, Line, Text, Tooltip, TooltipProvider } from 'folds';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
import { callChatAtom } from '../../state/callEmbed';
|
||||||
|
|
||||||
|
export function ControlDivider() {
|
||||||
|
return (
|
||||||
|
<Line variant="SurfaceVariant" size="300" direction="Vertical" className={css.ControlDivider} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type MicrophoneButtonProps = {
|
||||||
|
enabled: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
};
|
||||||
|
export function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
delay={500}
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={anchorRef}
|
||||||
|
variant={enabled ? 'Surface' : 'Warning'}
|
||||||
|
fill="Soft"
|
||||||
|
radii="400"
|
||||||
|
size="400"
|
||||||
|
onClick={() => onToggle()}
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
<Icon size="400" src={enabled ? Icons.Mic : Icons.MicMute} filled={!enabled} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SoundButtonProps = {
|
||||||
|
enabled: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
};
|
||||||
|
export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
delay={500}
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{enabled ? 'Turn Off Sound' : 'Turn On Sound'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={anchorRef}
|
||||||
|
variant={enabled ? 'Surface' : 'Warning'}
|
||||||
|
fill="Soft"
|
||||||
|
radii="400"
|
||||||
|
size="400"
|
||||||
|
onClick={() => onToggle()}
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
size="400"
|
||||||
|
src={enabled ? Icons.Headphone : Icons.HeadphoneMute}
|
||||||
|
filled={!enabled}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type VideoButtonProps = {
|
||||||
|
enabled: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
};
|
||||||
|
export function VideoButton({ enabled, onToggle }: VideoButtonProps) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
delay={500}
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{enabled ? 'Stop Camera' : 'Start Camera'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={anchorRef}
|
||||||
|
variant={enabled ? 'Success' : 'Surface'}
|
||||||
|
fill="Soft"
|
||||||
|
radii="400"
|
||||||
|
size="400"
|
||||||
|
onClick={() => onToggle()}
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
size="400"
|
||||||
|
src={enabled ? Icons.VideoCamera : Icons.VideoCameraMute}
|
||||||
|
filled={enabled}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScreenShareButtonProps = {
|
||||||
|
enabled: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
};
|
||||||
|
export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
delay={500}
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{enabled ? 'Stop Screenshare' : 'Start Screenshare'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={anchorRef}
|
||||||
|
variant={enabled ? 'Success' : 'Surface'}
|
||||||
|
fill="Soft"
|
||||||
|
radii="400"
|
||||||
|
size="400"
|
||||||
|
onClick={() => onToggle()}
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
<Icon size="400" src={Icons.ScreenShare} filled={enabled} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatButton() {
|
||||||
|
const [chat, setChat] = useAtom(callChatAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
delay={500}
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{chat ? 'Close Chat' : 'Open Chat'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={anchorRef}
|
||||||
|
variant={chat ? 'Success' : 'Surface'}
|
||||||
|
fill="Soft"
|
||||||
|
radii="400"
|
||||||
|
size="400"
|
||||||
|
onClick={() => setChat(!chat)}
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
<Icon size="400" src={Icons.Message} filled={chat} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
src/app/features/call/PrescreenControls.tsx
Normal file
67
src/app/features/call/PrescreenControls.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Button, Icon, Icons, Spinner, Text } from 'folds';
|
||||||
|
import { SequenceCard } from '../../components/sequence-card';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
import { ChatButton, ControlDivider, MicrophoneButton, SoundButton, VideoButton } from './Controls';
|
||||||
|
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
|
||||||
|
import { useCallEmbed, useCallJoined, useCallStart } from '../../hooks/useCallEmbed';
|
||||||
|
import { useCallPreferences } from '../../state/hooks/callPreferences';
|
||||||
|
|
||||||
|
type PrescreenControlsProps = {
|
||||||
|
canJoin?: boolean;
|
||||||
|
};
|
||||||
|
export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
|
||||||
|
const room = useRoom();
|
||||||
|
const callEmbed = useCallEmbed();
|
||||||
|
const callJoined = useCallJoined(callEmbed);
|
||||||
|
const direct = useIsDirectRoom();
|
||||||
|
|
||||||
|
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
|
||||||
|
|
||||||
|
const startCall = useCallStart(direct);
|
||||||
|
const joining = callEmbed?.roomId === room.roomId && !callJoined;
|
||||||
|
|
||||||
|
const disabled = inOtherCall || !canJoin;
|
||||||
|
|
||||||
|
const { microphone, video, sound, toggleMicrophone, toggleVideo, toggleSound } =
|
||||||
|
useCallPreferences();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SequenceCard
|
||||||
|
className={css.ControlCard}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
gap="400"
|
||||||
|
radii="500"
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="SpaceBetween"
|
||||||
|
wrap="Wrap"
|
||||||
|
>
|
||||||
|
<Box shrink="No" alignItems="Inherit" justifyContent="SpaceBetween" gap="200">
|
||||||
|
<MicrophoneButton enabled={microphone} onToggle={toggleMicrophone} />
|
||||||
|
<SoundButton enabled={sound} onToggle={toggleSound} />
|
||||||
|
</Box>
|
||||||
|
<ControlDivider />
|
||||||
|
<Box shrink="No" alignItems="Inherit" justifyContent="SpaceBetween" gap="200">
|
||||||
|
<VideoButton enabled={video} onToggle={toggleVideo} />
|
||||||
|
<ChatButton />
|
||||||
|
</Box>
|
||||||
|
<Box grow="Yes" direction="Column">
|
||||||
|
<Button
|
||||||
|
variant={disabled ? 'Secondary' : 'Success'}
|
||||||
|
fill={disabled ? 'Soft' : 'Solid'}
|
||||||
|
onClick={() => startCall(room, { microphone, video, sound })}
|
||||||
|
disabled={disabled || joining}
|
||||||
|
before={
|
||||||
|
joining ? (
|
||||||
|
<Spinner variant="Success" fill="Solid" size="200" />
|
||||||
|
) : (
|
||||||
|
<Icon src={Icons.Phone} size="200" filled />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="B400">Join</Text>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</SequenceCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
src/app/features/call/styles.css.ts
Normal file
28
src/app/features/call/styles.css.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
import { config, toRem } from 'folds';
|
||||||
|
|
||||||
|
export const CallViewContent = style({
|
||||||
|
padding: config.space.S400,
|
||||||
|
paddingRight: 0,
|
||||||
|
minHeight: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ControlCard = style({
|
||||||
|
padding: config.space.S300,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ControlDivider = style({
|
||||||
|
height: toRem(24),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CallMemberCard = style({
|
||||||
|
padding: config.space.S300,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CallControlContainer = style({
|
||||||
|
padding: config.space.S400,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PrescreenMessage = style({
|
||||||
|
padding: config.space.S200,
|
||||||
|
});
|
||||||
@@ -6,9 +6,8 @@ import { useAtomValue } from 'jotai';
|
|||||||
import {
|
import {
|
||||||
ExtendedJoinRules,
|
ExtendedJoinRules,
|
||||||
JoinRulesSwitcher,
|
JoinRulesSwitcher,
|
||||||
useRoomJoinRuleIcon,
|
useJoinRuleIcons,
|
||||||
useRoomJoinRuleLabel,
|
useRoomJoinRuleLabel,
|
||||||
useSpaceJoinRuleIcon,
|
|
||||||
} from '../../../components/JoinRulesSwitcher';
|
} from '../../../components/JoinRulesSwitcher';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||||
@@ -75,8 +74,7 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
|
|||||||
return r;
|
return r;
|
||||||
}, [allowKnockRestricted, allowRestricted, allowKnock, space]);
|
}, [allowKnockRestricted, allowRestricted, allowKnock, space]);
|
||||||
|
|
||||||
const icons = useRoomJoinRuleIcon();
|
const icons = useJoinRuleIcons(room.getType());
|
||||||
const spaceIcons = useSpaceJoinRuleIcon();
|
|
||||||
const labels = useRoomJoinRuleLabel();
|
const labels = useRoomJoinRuleLabel();
|
||||||
|
|
||||||
const [submitState, submit] = useAsyncCallback(
|
const [submitState, submit] = useAsyncCallback(
|
||||||
@@ -137,7 +135,7 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
|
|||||||
}
|
}
|
||||||
after={
|
after={
|
||||||
<JoinRulesSwitcher
|
<JoinRulesSwitcher
|
||||||
icons={room.isSpaceRoom() ? spaceIcons : icons}
|
icons={icons}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
rules={joinRules}
|
rules={joinRules}
|
||||||
value={rule}
|
value={rule}
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export function RoomProfileEdit({
|
|||||||
alt={name}
|
alt={name}
|
||||||
renderFallback={() => (
|
renderFallback={() => (
|
||||||
<RoomIcon
|
<RoomIcon
|
||||||
space={room.isSpaceRoom()}
|
roomType={room.getType()}
|
||||||
size="400"
|
size="400"
|
||||||
joinRule={joinRule?.join_rule ?? JoinRule.Invite}
|
joinRule={joinRule?.join_rule ?? JoinRule.Invite}
|
||||||
filled
|
filled
|
||||||
@@ -342,7 +342,7 @@ export function RoomProfile({ permissions }: RoomProfileProps) {
|
|||||||
alt={name}
|
alt={name}
|
||||||
renderFallback={() => (
|
renderFallback={() => (
|
||||||
<RoomIcon
|
<RoomIcon
|
||||||
space={room.isSpaceRoom()}
|
roomType={room.getType()}
|
||||||
size="400"
|
size="400"
|
||||||
joinRule={joinRule?.join_rule ?? JoinRule.Invite}
|
joinRule={joinRule?.join_rule ?? JoinRule.Invite}
|
||||||
filled
|
filled
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
|
import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
|
||||||
import { MatrixError, Room } from 'matrix-js-sdk';
|
import { MatrixError, Room, JoinRule } from 'matrix-js-sdk';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -33,24 +33,43 @@ import {
|
|||||||
createRoom,
|
createRoom,
|
||||||
CreateRoomAliasInput,
|
CreateRoomAliasInput,
|
||||||
CreateRoomData,
|
CreateRoomData,
|
||||||
CreateRoomKind,
|
CreateRoomAccess,
|
||||||
CreateRoomKindSelector,
|
CreateRoomAccessSelector,
|
||||||
RoomVersionSelector,
|
RoomVersionSelector,
|
||||||
useAdditionalCreators,
|
useAdditionalCreators,
|
||||||
|
CreateRoomType,
|
||||||
} from '../../components/create-room';
|
} from '../../components/create-room';
|
||||||
|
import { RoomType } from '../../../types/matrix/room';
|
||||||
|
import { CreateRoomTypeSelector } from '../../components/create-room/CreateRoomTypeSelector';
|
||||||
|
import { getRoomIconSrc } from '../../utils/room';
|
||||||
|
|
||||||
const getCreateRoomKindToIcon = (kind: CreateRoomKind) => {
|
const getCreateRoomAccessToIcon = (access: CreateRoomAccess, type?: CreateRoomType) => {
|
||||||
if (kind === CreateRoomKind.Private) return Icons.HashLock;
|
const isVoiceRoom = type === CreateRoomType.VoiceRoom;
|
||||||
if (kind === CreateRoomKind.Restricted) return Icons.Hash;
|
|
||||||
return Icons.HashGlobe;
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CreateRoomFormProps = {
|
type CreateRoomFormProps = {
|
||||||
defaultKind?: CreateRoomKind;
|
defaultAccess?: CreateRoomAccess;
|
||||||
|
defaultType?: CreateRoomType;
|
||||||
space?: Room;
|
space?: Room;
|
||||||
onCreate?: (roomId: string) => void;
|
onCreate?: (roomId: string) => void;
|
||||||
};
|
};
|
||||||
export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormProps) {
|
export function CreateRoomForm({
|
||||||
|
defaultAccess,
|
||||||
|
defaultType,
|
||||||
|
space,
|
||||||
|
onCreate,
|
||||||
|
}: CreateRoomFormProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const alive = useAlive();
|
const alive = useAlive();
|
||||||
|
|
||||||
@@ -64,8 +83,9 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
|
|||||||
|
|
||||||
const allowRestricted = space && restrictedSupported(selectedRoomVersion);
|
const allowRestricted = space && restrictedSupported(selectedRoomVersion);
|
||||||
|
|
||||||
const [kind, setKind] = useState(
|
const [type, setType] = useState(defaultType ?? CreateRoomType.TextRoom);
|
||||||
defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private
|
const [access, setAccess] = useState(
|
||||||
|
defaultAccess ?? (allowRestricted ? CreateRoomAccess.Restricted : CreateRoomAccess.Private)
|
||||||
);
|
);
|
||||||
const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
|
const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
|
||||||
const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } =
|
const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } =
|
||||||
@@ -75,13 +95,13 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
|
|||||||
const [knock, setKnock] = useState(false);
|
const [knock, setKnock] = useState(false);
|
||||||
const [advance, setAdvance] = useState(false);
|
const [advance, setAdvance] = useState(false);
|
||||||
|
|
||||||
const allowKnock = kind === CreateRoomKind.Private && knockSupported(selectedRoomVersion);
|
const allowKnock = access === CreateRoomAccess.Private && knockSupported(selectedRoomVersion);
|
||||||
const allowKnockRestricted =
|
const allowKnockRestricted =
|
||||||
kind === CreateRoomKind.Restricted && knockRestrictedSupported(selectedRoomVersion);
|
access === CreateRoomAccess.Restricted && knockRestrictedSupported(selectedRoomVersion);
|
||||||
|
|
||||||
const handleRoomVersionChange = (version: string) => {
|
const handleRoomVersionChange = (version: string) => {
|
||||||
if (!restrictedSupported(version)) {
|
if (!restrictedSupported(version)) {
|
||||||
setKind(CreateRoomKind.Private);
|
setAccess(CreateRoomAccess.Private);
|
||||||
}
|
}
|
||||||
selectRoomVersion(version);
|
selectRoomVersion(version);
|
||||||
};
|
};
|
||||||
@@ -107,19 +127,23 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
|
|||||||
aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined;
|
aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined;
|
||||||
|
|
||||||
if (!roomName) return;
|
if (!roomName) return;
|
||||||
const publicRoom = kind === CreateRoomKind.Public;
|
const publicRoom = access === CreateRoomAccess.Public;
|
||||||
let roomKnock = false;
|
let roomKnock = false;
|
||||||
if (allowKnock && kind === CreateRoomKind.Private) {
|
if (allowKnock && access === CreateRoomAccess.Private) {
|
||||||
roomKnock = knock;
|
roomKnock = knock;
|
||||||
}
|
}
|
||||||
if (allowKnockRestricted && kind === CreateRoomKind.Restricted) {
|
if (allowKnockRestricted && access === CreateRoomAccess.Restricted) {
|
||||||
roomKnock = knock;
|
roomKnock = knock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let roomType: RoomType | undefined;
|
||||||
|
if (type === CreateRoomType.VoiceRoom) roomType = RoomType.Call;
|
||||||
|
|
||||||
create({
|
create({
|
||||||
version: selectedRoomVersion,
|
version: selectedRoomVersion,
|
||||||
|
type: roomType,
|
||||||
parent: space,
|
parent: space,
|
||||||
kind,
|
access,
|
||||||
name: roomName,
|
name: roomName,
|
||||||
topic: roomTopic || undefined,
|
topic: roomTopic || undefined,
|
||||||
aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
|
aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
|
||||||
@@ -136,21 +160,32 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500">
|
<Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500">
|
||||||
|
{!space && (
|
||||||
|
<Box direction="Column" gap="100">
|
||||||
|
<Text size="L400">Type</Text>
|
||||||
|
<CreateRoomTypeSelector
|
||||||
|
value={type}
|
||||||
|
onSelect={setType}
|
||||||
|
disabled={disabled}
|
||||||
|
getIcon={getCreateRoomTypeToIcon}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">Access</Text>
|
<Text size="L400">Access</Text>
|
||||||
<CreateRoomKindSelector
|
<CreateRoomAccessSelector
|
||||||
value={kind}
|
value={access}
|
||||||
onSelect={setKind}
|
onSelect={setAccess}
|
||||||
canRestrict={allowRestricted}
|
canRestrict={allowRestricted}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
getIcon={getCreateRoomKindToIcon}
|
getIcon={(roomAccess) => getCreateRoomAccessToIcon(roomAccess, type)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No" direction="Column" gap="100">
|
<Box shrink="No" direction="Column" gap="100">
|
||||||
<Text size="L400">Name</Text>
|
<Text size="L400">Name</Text>
|
||||||
<Input
|
<Input
|
||||||
required
|
required
|
||||||
before={<Icon size="100" src={getCreateRoomKindToIcon(kind)} />}
|
before={<Icon size="100" src={getCreateRoomAccessToIcon(access, type)} />}
|
||||||
name="nameInput"
|
name="nameInput"
|
||||||
autoFocus
|
autoFocus
|
||||||
size="500"
|
size="500"
|
||||||
@@ -171,7 +206,7 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{kind === CreateRoomKind.Public && <CreateRoomAliasInput disabled={disabled} />}
|
{access === CreateRoomAccess.Public && <CreateRoomAliasInput disabled={disabled} />}
|
||||||
|
|
||||||
<Box shrink="No" direction="Column" gap="100">
|
<Box shrink="No" direction="Column" gap="100">
|
||||||
<Box gap="200" alignItems="End">
|
<Box gap="200" alignItems="End">
|
||||||
@@ -183,7 +218,7 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
|
|||||||
onClick={() => setAdvance(!advance)}
|
onClick={() => setAdvance(!advance)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Text size="T200">Advance Options</Text>
|
<Text size="T200">Advanced Options</Text>
|
||||||
</Chip>
|
</Chip>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -201,7 +236,7 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
|
|||||||
/>
|
/>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
)}
|
)}
|
||||||
{kind !== CreateRoomKind.Public && (
|
{access !== CreateRoomAccess.Public && (
|
||||||
<>
|
<>
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
style={{ padding: config.space.S300 }}
|
style={{ padding: config.space.S300 }}
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ import {
|
|||||||
} from '../../state/hooks/createRoomModal';
|
} from '../../state/hooks/createRoomModal';
|
||||||
import { CreateRoomModalState } from '../../state/createRoomModal';
|
import { CreateRoomModalState } from '../../state/createRoomModal';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
import { CreateRoomType } from '../../components/create-room/types';
|
||||||
|
|
||||||
type CreateRoomModalProps = {
|
type CreateRoomModalProps = {
|
||||||
state: CreateRoomModalState;
|
state: CreateRoomModalState;
|
||||||
};
|
};
|
||||||
function CreateRoomModal({ state }: CreateRoomModalProps) {
|
function CreateRoomModal({ state }: CreateRoomModalProps) {
|
||||||
const { spaceId } = state;
|
const { spaceId, type } = state;
|
||||||
const closeDialog = useCloseCreateRoomModal();
|
const closeDialog = useCloseCreateRoomModal();
|
||||||
|
|
||||||
const allJoinedRooms = useAllJoinedRoomsSet();
|
const allJoinedRooms = useAllJoinedRoomsSet();
|
||||||
@@ -57,7 +58,9 @@ function CreateRoomModal({ state }: CreateRoomModalProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4">New Room</Text>
|
<Text size="H4">
|
||||||
|
{type === CreateRoomType.VoiceRoom ? 'New Voice Room' : 'New Chat Room'}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton size="300" radii="300" onClick={closeDialog}>
|
<IconButton size="300" radii="300" onClick={closeDialog}>
|
||||||
@@ -74,7 +77,7 @@ function CreateRoomModal({ state }: CreateRoomModalProps) {
|
|||||||
direction="Column"
|
direction="Column"
|
||||||
gap="500"
|
gap="500"
|
||||||
>
|
>
|
||||||
<CreateRoomForm space={space} onCreate={closeDialog} />
|
<CreateRoomForm space={space} onCreate={closeDialog} defaultType={type} />
|
||||||
</Box>
|
</Box>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -33,25 +33,25 @@ import {
|
|||||||
createRoom,
|
createRoom,
|
||||||
CreateRoomAliasInput,
|
CreateRoomAliasInput,
|
||||||
CreateRoomData,
|
CreateRoomData,
|
||||||
CreateRoomKind,
|
CreateRoomAccess,
|
||||||
CreateRoomKindSelector,
|
CreateRoomAccessSelector,
|
||||||
RoomVersionSelector,
|
RoomVersionSelector,
|
||||||
useAdditionalCreators,
|
useAdditionalCreators,
|
||||||
} from '../../components/create-room';
|
} from '../../components/create-room';
|
||||||
import { RoomType } from '../../../types/matrix/room';
|
import { RoomType } from '../../../types/matrix/room';
|
||||||
|
|
||||||
const getCreateSpaceKindToIcon = (kind: CreateRoomKind) => {
|
const getCreateSpaceAccessToIcon = (access: CreateRoomAccess) => {
|
||||||
if (kind === CreateRoomKind.Private) return Icons.SpaceLock;
|
if (access === CreateRoomAccess.Private) return Icons.SpaceLock;
|
||||||
if (kind === CreateRoomKind.Restricted) return Icons.Space;
|
if (access === CreateRoomAccess.Restricted) return Icons.Space;
|
||||||
return Icons.SpaceGlobe;
|
return Icons.SpaceGlobe;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CreateSpaceFormProps = {
|
type CreateSpaceFormProps = {
|
||||||
defaultKind?: CreateRoomKind;
|
defaultAccess?: CreateRoomAccess;
|
||||||
space?: Room;
|
space?: Room;
|
||||||
onCreate?: (roomId: string) => void;
|
onCreate?: (roomId: string) => void;
|
||||||
};
|
};
|
||||||
export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFormProps) {
|
export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceFormProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const alive = useAlive();
|
const alive = useAlive();
|
||||||
|
|
||||||
@@ -65,8 +65,8 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
|
|||||||
|
|
||||||
const allowRestricted = space && restrictedSupported(selectedRoomVersion);
|
const allowRestricted = space && restrictedSupported(selectedRoomVersion);
|
||||||
|
|
||||||
const [kind, setKind] = useState(
|
const [access, setAccess] = useState(
|
||||||
defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private
|
defaultAccess ?? (allowRestricted ? CreateRoomAccess.Restricted : CreateRoomAccess.Private)
|
||||||
);
|
);
|
||||||
|
|
||||||
const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
|
const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
|
||||||
@@ -76,13 +76,13 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
|
|||||||
const [knock, setKnock] = useState(false);
|
const [knock, setKnock] = useState(false);
|
||||||
const [advance, setAdvance] = useState(false);
|
const [advance, setAdvance] = useState(false);
|
||||||
|
|
||||||
const allowKnock = kind === CreateRoomKind.Private && knockSupported(selectedRoomVersion);
|
const allowKnock = access === CreateRoomAccess.Private && knockSupported(selectedRoomVersion);
|
||||||
const allowKnockRestricted =
|
const allowKnockRestricted =
|
||||||
kind === CreateRoomKind.Restricted && knockRestrictedSupported(selectedRoomVersion);
|
access === CreateRoomAccess.Restricted && knockRestrictedSupported(selectedRoomVersion);
|
||||||
|
|
||||||
const handleRoomVersionChange = (version: string) => {
|
const handleRoomVersionChange = (version: string) => {
|
||||||
if (!restrictedSupported(version)) {
|
if (!restrictedSupported(version)) {
|
||||||
setKind(CreateRoomKind.Private);
|
setAccess(CreateRoomAccess.Private);
|
||||||
}
|
}
|
||||||
selectRoomVersion(version);
|
selectRoomVersion(version);
|
||||||
};
|
};
|
||||||
@@ -108,12 +108,12 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
|
|||||||
aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined;
|
aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined;
|
||||||
|
|
||||||
if (!roomName) return;
|
if (!roomName) return;
|
||||||
const publicRoom = kind === CreateRoomKind.Public;
|
const publicRoom = access === CreateRoomAccess.Public;
|
||||||
let roomKnock = false;
|
let roomKnock = false;
|
||||||
if (allowKnock && kind === CreateRoomKind.Private) {
|
if (allowKnock && access === CreateRoomAccess.Private) {
|
||||||
roomKnock = knock;
|
roomKnock = knock;
|
||||||
}
|
}
|
||||||
if (allowKnockRestricted && kind === CreateRoomKind.Restricted) {
|
if (allowKnockRestricted && access === CreateRoomAccess.Restricted) {
|
||||||
roomKnock = knock;
|
roomKnock = knock;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
|
|||||||
version: selectedRoomVersion,
|
version: selectedRoomVersion,
|
||||||
type: RoomType.Space,
|
type: RoomType.Space,
|
||||||
parent: space,
|
parent: space,
|
||||||
kind,
|
access,
|
||||||
name: roomName,
|
name: roomName,
|
||||||
topic: roomTopic || undefined,
|
topic: roomTopic || undefined,
|
||||||
aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
|
aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
|
||||||
@@ -139,19 +139,19 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
|
|||||||
<Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500">
|
<Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500">
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">Access</Text>
|
<Text size="L400">Access</Text>
|
||||||
<CreateRoomKindSelector
|
<CreateRoomAccessSelector
|
||||||
value={kind}
|
value={access}
|
||||||
onSelect={setKind}
|
onSelect={setAccess}
|
||||||
canRestrict={allowRestricted}
|
canRestrict={allowRestricted}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
getIcon={getCreateSpaceKindToIcon}
|
getIcon={getCreateSpaceAccessToIcon}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No" direction="Column" gap="100">
|
<Box shrink="No" direction="Column" gap="100">
|
||||||
<Text size="L400">Name</Text>
|
<Text size="L400">Name</Text>
|
||||||
<Input
|
<Input
|
||||||
required
|
required
|
||||||
before={<Icon size="100" src={getCreateSpaceKindToIcon(kind)} />}
|
before={<Icon size="100" src={getCreateSpaceAccessToIcon(access)} />}
|
||||||
name="nameInput"
|
name="nameInput"
|
||||||
autoFocus
|
autoFocus
|
||||||
size="500"
|
size="500"
|
||||||
@@ -172,7 +172,7 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{kind === CreateRoomKind.Public && <CreateRoomAliasInput disabled={disabled} />}
|
{access === CreateRoomAccess.Public && <CreateRoomAliasInput disabled={disabled} />}
|
||||||
|
|
||||||
<Box shrink="No" direction="Column" gap="100">
|
<Box shrink="No" direction="Column" gap="100">
|
||||||
<Box gap="200" alignItems="End">
|
<Box gap="200" alignItems="End">
|
||||||
@@ -184,7 +184,7 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
|
|||||||
onClick={() => setAdvance(!advance)}
|
onClick={() => setAdvance(!advance)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Text size="T200">Advance Options</Text>
|
<Text size="T200">Advanced Options</Text>
|
||||||
</Chip>
|
</Chip>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -202,7 +202,7 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
|
|||||||
/>
|
/>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
)}
|
)}
|
||||||
{kind !== CreateRoomKind.Public && advance && (allowKnock || allowKnockRestricted) && (
|
{access !== CreateRoomAccess.Public && advance && (allowKnock || allowKnockRestricted) && (
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
style={{ padding: config.space.S300 }}
|
style={{ padding: config.space.S300 }}
|
||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
|||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<BackRouteHandler>
|
<BackRouteHandler>
|
||||||
{(onBack) => (
|
{(onBack) => (
|
||||||
<IconButton onClick={onBack}>
|
<IconButton fill="None" onClick={onBack}>
|
||||||
<Icon src={Icons.ArrowLeft} />
|
<Icon src={Icons.ArrowLeft} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
@@ -218,7 +218,11 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(triggerRef) => (
|
{(triggerRef) => (
|
||||||
<IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}>
|
<IconButton
|
||||||
|
fill="None"
|
||||||
|
ref={triggerRef}
|
||||||
|
onClick={() => setPeopleDrawer((drawer) => !drawer)}
|
||||||
|
>
|
||||||
<Icon size="400" src={Icons.User} />
|
<Icon size="400" src={Icons.User} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
@@ -235,7 +239,12 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(triggerRef) => (
|
{(triggerRef) => (
|
||||||
<IconButton onClick={handleOpenMenu} ref={triggerRef} aria-pressed={!!menuAnchor}>
|
<IconButton
|
||||||
|
fill="None"
|
||||||
|
onClick={handleOpenMenu}
|
||||||
|
ref={triggerRef}
|
||||||
|
aria-pressed={!!menuAnchor}
|
||||||
|
>
|
||||||
<Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} />
|
<Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf
|
|||||||
|
|
||||||
type RoomProfileProps = {
|
type RoomProfileProps = {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
roomType?: string;
|
||||||
name: string;
|
name: string;
|
||||||
topic?: string;
|
topic?: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
@@ -185,6 +186,7 @@ type RoomProfileProps = {
|
|||||||
};
|
};
|
||||||
function RoomProfile({
|
function RoomProfile({
|
||||||
roomId,
|
roomId,
|
||||||
|
roomType,
|
||||||
name,
|
name,
|
||||||
topic,
|
topic,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
@@ -200,9 +202,7 @@ function RoomProfile({
|
|||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
alt={name}
|
alt={name}
|
||||||
renderFallback={() => (
|
renderFallback={() => <RoomIcon size="300" joinRule={joinRule} roomType={roomType} />}
|
||||||
<RoomIcon size="300" joinRule={joinRule ?? JoinRule.Restricted} filled />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
@@ -338,6 +338,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
|
|||||||
{(localSummary) => (
|
{(localSummary) => (
|
||||||
<RoomProfile
|
<RoomProfile
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
|
roomType={localSummary.roomType}
|
||||||
name={localSummary.name}
|
name={localSummary.name}
|
||||||
topic={localSummary.topic}
|
topic={localSummary.topic}
|
||||||
avatarUrl={
|
avatarUrl={
|
||||||
@@ -396,6 +397,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
|
|||||||
{summary && (
|
{summary && (
|
||||||
<RoomProfile
|
<RoomProfile
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
|
roomType={summary.room_type}
|
||||||
name={summary.name || summary.canonical_alias || roomId}
|
name={summary.name || summary.canonical_alias || roomId}
|
||||||
topic={summary.topic}
|
topic={summary.topic}
|
||||||
avatarUrl={
|
avatarUrl={
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
|||||||
import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal';
|
import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal';
|
||||||
import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal';
|
import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal';
|
||||||
import { AddExistingModal } from '../add-existing';
|
import { AddExistingModal } from '../add-existing';
|
||||||
|
import { CreateRoomType } from '../../components/create-room/types';
|
||||||
|
import { BetaNoticeBadge } from '../../components/BetaNoticeBadge';
|
||||||
|
|
||||||
function SpaceProfileLoading() {
|
function SpaceProfileLoading() {
|
||||||
return (
|
return (
|
||||||
@@ -249,8 +251,8 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
|
|||||||
setCords(evt.currentTarget.getBoundingClientRect());
|
setCords(evt.currentTarget.getBoundingClientRect());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateRoom = () => {
|
const handleCreateRoom = (type?: CreateRoomType) => {
|
||||||
openCreateRoomModal(item.roomId);
|
openCreateRoomModal(item.roomId, type);
|
||||||
setCords(undefined);
|
setCords(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -281,9 +283,19 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
|
|||||||
radii="300"
|
radii="300"
|
||||||
variant="Primary"
|
variant="Primary"
|
||||||
fill="None"
|
fill="None"
|
||||||
onClick={handleCreateRoom}
|
onClick={() => handleCreateRoom(CreateRoomType.TextRoom)}
|
||||||
>
|
>
|
||||||
<Text size="T300">New Room</Text>
|
<Text size="T300">Chat Room</Text>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
variant="Primary"
|
||||||
|
fill="None"
|
||||||
|
onClick={() => handleCreateRoom(CreateRoomType.VoiceRoom)}
|
||||||
|
after={<BetaNoticeBadge />}
|
||||||
|
>
|
||||||
|
<Text size="T300">Voice Room</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem size="300" radii="300" fill="None" onClick={handleAddExisting}>
|
<MenuItem size="300" radii="300" fill="None" onClick={handleAddExisting}>
|
||||||
<Text size="T300">Existing Room</Text>
|
<Text size="T300">Existing Room</Text>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { SearchOrderBy } from 'matrix-js-sdk';
|
|||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { joinRuleToIconSrc } from '../../utils/room';
|
import { getRoomIconSrc } from '../../utils/room';
|
||||||
import { factoryRoomIdByAtoZ } from '../../utils/sort';
|
import { factoryRoomIdByAtoZ } from '../../utils/sort';
|
||||||
import {
|
import {
|
||||||
SearchItemStrGetter,
|
SearchItemStrGetter,
|
||||||
@@ -274,9 +274,7 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
|
|||||||
before={
|
before={
|
||||||
<Icon
|
<Icon
|
||||||
size="50"
|
size="50"
|
||||||
src={
|
src={getRoomIconSrc(Icons, room.getType(), room.getJoinRule())}
|
||||||
joinRuleToIconSrc(Icons, room.getJoinRule(), false) ?? Icons.Hash
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -392,10 +390,7 @@ export function SearchFilters({
|
|||||||
onClick={() => onSelectedRoomsChange(selectedRooms.filter((rId) => rId !== roomId))}
|
onClick={() => onSelectedRoomsChange(selectedRooms.filter((rId) => rId !== roomId))}
|
||||||
radii="Pill"
|
radii="Pill"
|
||||||
before={
|
before={
|
||||||
<Icon
|
<Icon size="50" src={getRoomIconSrc(Icons, room.getType(), room.getJoinRule())} />
|
||||||
size="50"
|
|
||||||
src={joinRuleToIconSrc(Icons, room.getJoinRule(), false) ?? Icons.Hash}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
after={<Icon size="50" src={Icons.Cross} />}
|
after={<Icon size="50" src={Icons.Cross} />}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -203,7 +203,12 @@ export function SearchResultGroup({
|
|||||||
src={getRoomAvatarUrl(mx, room, 96, useAuthentication)}
|
src={getRoomAvatarUrl(mx, room, 96, useAuthentication)}
|
||||||
alt={room.name}
|
alt={room.name}
|
||||||
renderFallback={() => (
|
renderFallback={() => (
|
||||||
<RoomIcon size="50" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled />
|
<RoomIcon
|
||||||
|
size="50"
|
||||||
|
roomType={room.getType()}
|
||||||
|
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
|
||||||
|
filled
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { useFocusWithin, useHover } from 'react-aria';
|
import { useFocusWithin, useHover } from 'react-aria';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
|
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
|
||||||
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
|
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
|
||||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||||
@@ -51,6 +52,13 @@ import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationS
|
|||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||||
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||||
|
import { useRoomName } from '../../hooks/useRoomMeta';
|
||||||
|
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
||||||
|
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
|
||||||
|
import { callChatAtom } from '../../state/callEmbed';
|
||||||
|
import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
|
||||||
|
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
|
||||||
|
import { livekitSupport } from '../../hooks/useLivekitSupport';
|
||||||
|
|
||||||
type RoomNavItemMenuProps = {
|
type RoomNavItemMenuProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
@@ -209,6 +217,24 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function CallChatToggle() {
|
||||||
|
const [chat, setChat] = useAtom(callChatAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setChat(!chat)}
|
||||||
|
aria-pressed={chat}
|
||||||
|
aria-label="Toggle Chat"
|
||||||
|
variant="Background"
|
||||||
|
fill="None"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<Icon size="50" src={Icons.Message} filled={chat} />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type RoomNavItemProps = {
|
type RoomNavItemProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
@@ -236,6 +262,8 @@ export function RoomNavItem({
|
|||||||
(receipt) => receipt.userId !== mx.getUserId()
|
(receipt) => receipt.userId !== mx.getUserId()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const roomName = useRoomName(room);
|
||||||
|
|
||||||
const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => {
|
const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
setMenuAnchor({
|
setMenuAnchor({
|
||||||
@@ -251,6 +279,29 @@ export function RoomNavItem({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const optionsVisible = hover || !!menuAnchor;
|
const optionsVisible = hover || !!menuAnchor;
|
||||||
|
const callSession = useCallSession(room);
|
||||||
|
const callMembers = useCallMembers(room, callSession);
|
||||||
|
const startCall = useCallStart(direct);
|
||||||
|
const callEmbed = useCallEmbed();
|
||||||
|
const callPref = useAtomValue(useCallPreferencesAtom());
|
||||||
|
const autoDiscoveryInfo = useAutoDiscoveryInfo();
|
||||||
|
|
||||||
|
const handleStartCall: MouseEventHandler<HTMLAnchorElement> = (evt) => {
|
||||||
|
// Do not join if no livekit support or call is not started by others
|
||||||
|
if (!livekitSupport(autoDiscoveryInfo) && callMembers.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not join if already in call
|
||||||
|
if (callEmbed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Start call in second click
|
||||||
|
if (selected) {
|
||||||
|
evt.preventDefault();
|
||||||
|
startCall(room, callPref);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavItem
|
<NavItem
|
||||||
@@ -263,7 +314,7 @@ export function RoomNavItem({
|
|||||||
{...hoverProps}
|
{...hoverProps}
|
||||||
{...focusWithinProps}
|
{...focusWithinProps}
|
||||||
>
|
>
|
||||||
<NavLink to={linkPath}>
|
<NavLink to={linkPath} onClick={room.isCallRoom() ? handleStartCall : undefined}>
|
||||||
<NavItemContent>
|
<NavItemContent>
|
||||||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
||||||
<Avatar size="200" radii="400">
|
<Avatar size="200" radii="400">
|
||||||
@@ -275,25 +326,28 @@ export function RoomNavItem({
|
|||||||
? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
|
? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||||
: getRoomAvatarUrl(mx, room, 96, useAuthentication)
|
: getRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||||
}
|
}
|
||||||
alt={room.name}
|
alt={roomName}
|
||||||
renderFallback={() => (
|
renderFallback={() => (
|
||||||
<Text as="span" size="H6">
|
<Text as="span" size="H6">
|
||||||
{nameInitials(room.name)}
|
{nameInitials(roomName)}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RoomIcon
|
<RoomIcon
|
||||||
style={{ opacity: unread ? config.opacity.P500 : config.opacity.P300 }}
|
style={{
|
||||||
|
opacity: unread ? config.opacity.P500 : config.opacity.P300,
|
||||||
|
}}
|
||||||
filled={selected}
|
filled={selected}
|
||||||
size="100"
|
size="100"
|
||||||
joinRule={room.getJoinRule()}
|
joinRule={room.getJoinRule()}
|
||||||
|
roomType={room.getType()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box as="span" grow="Yes">
|
<Box as="span" grow="Yes">
|
||||||
<Text priority={unread ? '500' : '300'} as="span" size="Inherit" truncate>
|
<Text priority={unread ? '500' : '300'} as="span" size="Inherit" truncate>
|
||||||
{room.name}
|
{roomName}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
{!optionsVisible && !unread && !selected && typingMember.length > 0 && (
|
{!optionsVisible && !unread && !selected && typingMember.length > 0 && (
|
||||||
@@ -307,14 +361,30 @@ export function RoomNavItem({
|
|||||||
</UnreadBadgeCenter>
|
</UnreadBadgeCenter>
|
||||||
)}
|
)}
|
||||||
{!optionsVisible && notificationMode !== RoomNotificationMode.Unset && (
|
{!optionsVisible && notificationMode !== RoomNotificationMode.Unset && (
|
||||||
<Icon size="50" src={getRoomNotificationModeIcon(notificationMode)} />
|
<Icon
|
||||||
|
size="50"
|
||||||
|
src={getRoomNotificationModeIcon(notificationMode)}
|
||||||
|
aria-label={notificationMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{room.isCallRoom() && callMembers.length > 0 && (
|
||||||
|
<Badge variant="Critical" fill="Solid" size="400">
|
||||||
|
<Text as="span" size="L400" truncate>
|
||||||
|
{callMembers.length} Live
|
||||||
|
</Text>
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</NavItemContent>
|
</NavItemContent>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{optionsVisible && (
|
{optionsVisible && (
|
||||||
<NavItemOptions>
|
<NavItemOptions>
|
||||||
|
{selected && (callEmbed?.roomId === room.roomId || room.isCallRoom()) && (
|
||||||
|
<CallChatToggle />
|
||||||
|
)}
|
||||||
<PopOut
|
<PopOut
|
||||||
|
id={`menu-${room.roomId}`}
|
||||||
|
aria-expanded={!!menuAnchor}
|
||||||
anchor={menuAnchor}
|
anchor={menuAnchor}
|
||||||
offset={menuAnchor?.width === 0 ? 0 : undefined}
|
offset={menuAnchor?.width === 0 ? 0 : undefined}
|
||||||
alignOffset={menuAnchor?.width === 0 ? 0 : -5}
|
alignOffset={menuAnchor?.width === 0 ? 0 : -5}
|
||||||
@@ -343,6 +413,8 @@ export function RoomNavItem({
|
|||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleOpenMenu}
|
onClick={handleOpenMenu}
|
||||||
aria-pressed={!!menuAnchor}
|
aria-pressed={!!menuAnchor}
|
||||||
|
aria-controls={`menu-${room.roomId}`}
|
||||||
|
aria-label="More Options"
|
||||||
variant="Background"
|
variant="Background"
|
||||||
fill="None"
|
fill="None"
|
||||||
size="300"
|
size="300"
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
|
|||||||
renderFallback={() => (
|
renderFallback={() => (
|
||||||
<RoomIcon
|
<RoomIcon
|
||||||
size="50"
|
size="50"
|
||||||
|
roomType={room.getType()}
|
||||||
joinRule={joinRuleContent?.join_rule ?? JoinRule.Invite}
|
joinRule={joinRuleContent?.join_rule ?? JoinRule.Invite}
|
||||||
filled
|
filled
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function General({ requestClose }: GeneralProps) {
|
|||||||
<RoomLocalAddresses permissions={permissions} />
|
<RoomLocalAddresses permissions={permissions} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">Advance Options</Text>
|
<Text size="L400">Advanced Options</Text>
|
||||||
<RoomUpgrade permissions={permissions} requestClose={requestClose} />
|
<RoomUpgrade permissions={permissions} requestClose={requestClose} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function Permissions({ requestClose }: PermissionsProps) {
|
|||||||
|
|
||||||
const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId());
|
const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId());
|
||||||
const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId());
|
const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId());
|
||||||
const permissionGroups = usePermissionGroups();
|
const permissionGroups = usePermissionGroups(room.isCallRoom());
|
||||||
|
|
||||||
const [powerEditor, setPowerEditor] = useState(false);
|
const [powerEditor, setPowerEditor] = useState(false);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
|||||||
import { MessageEvent, StateEvent } from '../../../../types/matrix/room';
|
import { MessageEvent, StateEvent } from '../../../../types/matrix/room';
|
||||||
import { PermissionGroup } from '../../common-settings/permissions';
|
import { PermissionGroup } from '../../common-settings/permissions';
|
||||||
|
|
||||||
export const usePermissionGroups = (): PermissionGroup[] => {
|
export const usePermissionGroups = (isCallRoom: boolean): PermissionGroup[] => {
|
||||||
const groups: PermissionGroup[] = useMemo(() => {
|
const groups: PermissionGroup[] = useMemo(() => {
|
||||||
const messagesGroup: PermissionGroup = {
|
const messagesGroup: PermissionGroup = {
|
||||||
name: 'Messages',
|
name: 'Messages',
|
||||||
@@ -46,6 +46,19 @@ export const usePermissionGroups = (): PermissionGroup[] => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const callSettingsGroup: PermissionGroup = {
|
||||||
|
name: 'Calls',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
location: {
|
||||||
|
state: true,
|
||||||
|
key: StateEvent.GroupCallMemberPrefix,
|
||||||
|
},
|
||||||
|
name: 'Join Call',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const moderationGroup: PermissionGroup = {
|
const moderationGroup: PermissionGroup = {
|
||||||
name: 'Moderation',
|
name: 'Moderation',
|
||||||
items: [
|
items: [
|
||||||
@@ -177,6 +190,13 @@ export const usePermissionGroups = (): PermissionGroup[] => {
|
|||||||
const otherSettingsGroup: PermissionGroup = {
|
const otherSettingsGroup: PermissionGroup = {
|
||||||
name: 'Other',
|
name: 'Other',
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
location: {
|
||||||
|
state: true,
|
||||||
|
key: StateEvent.PoniesRoomEmotes,
|
||||||
|
},
|
||||||
|
name: 'Manage Emojis & Stickers',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
location: {
|
location: {
|
||||||
state: true,
|
state: true,
|
||||||
@@ -196,12 +216,13 @@ export const usePermissionGroups = (): PermissionGroup[] => {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
messagesGroup,
|
messagesGroup,
|
||||||
|
...(isCallRoom ? [callSettingsGroup] : []),
|
||||||
moderationGroup,
|
moderationGroup,
|
||||||
roomOverviewGroup,
|
roomOverviewGroup,
|
||||||
roomSettingsGroup,
|
roomSettingsGroup,
|
||||||
otherSettingsGroup,
|
otherSettingsGroup,
|
||||||
];
|
];
|
||||||
}, []);
|
}, [isCallRoom]);
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
};
|
};
|
||||||
|
|||||||
57
src/app/features/room/CallChatView.tsx
Normal file
57
src/app/features/room/CallChatView.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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 (
|
||||||
|
<Page
|
||||||
|
style={{
|
||||||
|
width: screenSize === ScreenSize.Desktop ? toRem(456) : '100%',
|
||||||
|
flexShrink: 0,
|
||||||
|
flexGrow: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PageHeader>
|
||||||
|
<Box grow="Yes" alignItems="Center" gap="200">
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Text size="H5" truncate>
|
||||||
|
Chat
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box shrink="No" alignItems="Center">
|
||||||
|
<TooltipProvider
|
||||||
|
position="Bottom"
|
||||||
|
align="End"
|
||||||
|
offset={4}
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text>Close</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(triggerRef) => (
|
||||||
|
<IconButton ref={triggerRef} variant="Surface" onClick={handleClose}>
|
||||||
|
<Icon src={Icons.Cross} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</PageHeader>
|
||||||
|
<Box grow="Yes" direction="Column">
|
||||||
|
<RoomView eventId={eventId} />
|
||||||
|
</Box>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
|
|||||||
import { Box, Line } from 'folds';
|
import { Box, Line } from 'folds';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { RoomView } from './RoomView';
|
import { RoomView } from './RoomView';
|
||||||
import { MembersDrawer } from './MembersDrawer';
|
import { MembersDrawer } from './MembersDrawer';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
@@ -13,6 +14,10 @@ import { useKeyDown } from '../../hooks/useKeyDown';
|
|||||||
import { markAsRead } from '../../utils/notifications';
|
import { markAsRead } from '../../utils/notifications';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
||||||
|
import { CallView } from '../call/CallView';
|
||||||
|
import { RoomViewHeader } from './RoomViewHeader';
|
||||||
|
import { callChatAtom } from '../../state/callEmbed';
|
||||||
|
import { CallChatView } from './CallChatView';
|
||||||
|
|
||||||
export function Room() {
|
export function Room() {
|
||||||
const { eventId } = useParams();
|
const { eventId } = useParams();
|
||||||
@@ -24,6 +29,7 @@ export function Room() {
|
|||||||
const screenSize = useScreenSizeContext();
|
const screenSize = useScreenSizeContext();
|
||||||
const powerLevels = usePowerLevels(room);
|
const powerLevels = usePowerLevels(room);
|
||||||
const members = useRoomMembers(mx, room.roomId);
|
const members = useRoomMembers(mx, room.roomId);
|
||||||
|
const chat = useAtomValue(callChatAtom);
|
||||||
|
|
||||||
useKeyDown(
|
useKeyDown(
|
||||||
window,
|
window,
|
||||||
@@ -37,11 +43,37 @@ export function Room() {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const callView = room.isCallRoom();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PowerLevelsContextProvider value={powerLevels}>
|
<PowerLevelsContextProvider value={powerLevels}>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<RoomView room={room} eventId={eventId} />
|
{callView && (screenSize === ScreenSize.Desktop || !chat) && (
|
||||||
{screenSize === ScreenSize.Desktop && isDrawer && (
|
<Box grow="Yes" direction="Column">
|
||||||
|
<RoomViewHeader callView />
|
||||||
|
<Box grow="Yes">
|
||||||
|
<CallView />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{!callView && (
|
||||||
|
<Box grow="Yes" direction="Column">
|
||||||
|
<RoomViewHeader />
|
||||||
|
<Box grow="Yes">
|
||||||
|
<RoomView eventId={eventId} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{callView && chat && (
|
||||||
|
<>
|
||||||
|
{screenSize === ScreenSize.Desktop && (
|
||||||
|
<Line variant="Background" direction="Vertical" size="300" />
|
||||||
|
)}
|
||||||
|
<CallChatView />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!callView && screenSize === ScreenSize.Desktop && isDrawer && (
|
||||||
<>
|
<>
|
||||||
<Line variant="Background" direction="Vertical" size="300" />
|
<Line variant="Background" direction="Vertical" size="300" />
|
||||||
<MembersDrawer key={room.roomId} room={room} members={members} />
|
<MembersDrawer key={room.roomId} room={room} members={members} />
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
const isComposing = useComposingCheck();
|
const isComposing = useComposingCheck();
|
||||||
|
|
||||||
useElementSizeObserver(
|
useElementSizeObserver(
|
||||||
useCallback(() => document.body, []),
|
useCallback(() => fileDropContainerRef.current, [fileDropContainerRef]),
|
||||||
useCallback((width) => setHideStickerBtn(width < 500), [])
|
useCallback((width) => setHideStickerBtn(width < 500), [])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { HTMLReactParserOptions } from 'html-react-parser';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { ReactEditor } from 'slate-react';
|
import { ReactEditor } from 'slate-react';
|
||||||
import { Editor } from 'slate';
|
import { Editor } from 'slate';
|
||||||
|
import { SessionMembershipData } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
|
||||||
import to from 'await-to-js';
|
import to from 'await-to-js';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import {
|
import {
|
||||||
@@ -471,6 +472,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
const permissions = useRoomPermissions(creators, powerLevels);
|
const permissions = useRoomPermissions(creators, powerLevels);
|
||||||
|
|
||||||
const canRedact = permissions.action('redact', mx.getSafeUserId());
|
const canRedact = permissions.action('redact', mx.getSafeUserId());
|
||||||
|
const canDeleteOwn = permissions.event(MessageEvent.RoomRedaction, mx.getSafeUserId());
|
||||||
const canSendReaction = permissions.event(MessageEvent.Reaction, mx.getSafeUserId());
|
const canSendReaction = permissions.event(MessageEvent.Reaction, mx.getSafeUserId());
|
||||||
const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, mx.getSafeUserId());
|
const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, mx.getSafeUserId());
|
||||||
const [editId, setEditId] = useState<string>();
|
const [editId, setEditId] = useState<string>();
|
||||||
@@ -1047,7 +1049,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
collapse={collapse}
|
collapse={collapse}
|
||||||
highlight={highlighted}
|
highlight={highlighted}
|
||||||
edit={editId === mEventId}
|
edit={editId === mEventId}
|
||||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
|
||||||
canSendReaction={canSendReaction}
|
canSendReaction={canSendReaction}
|
||||||
canPinEvent={canPinEvent}
|
canPinEvent={canPinEvent}
|
||||||
imagePackRooms={imagePackRooms}
|
imagePackRooms={imagePackRooms}
|
||||||
@@ -1129,7 +1131,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
collapse={collapse}
|
collapse={collapse}
|
||||||
highlight={highlighted}
|
highlight={highlighted}
|
||||||
edit={editId === mEventId}
|
edit={editId === mEventId}
|
||||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
|
||||||
canSendReaction={canSendReaction}
|
canSendReaction={canSendReaction}
|
||||||
canPinEvent={canPinEvent}
|
canPinEvent={canPinEvent}
|
||||||
imagePackRooms={imagePackRooms}
|
imagePackRooms={imagePackRooms}
|
||||||
@@ -1247,7 +1249,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
messageLayout={messageLayout}
|
messageLayout={messageLayout}
|
||||||
collapse={collapse}
|
collapse={collapse}
|
||||||
highlight={highlighted}
|
highlight={highlighted}
|
||||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
|
||||||
canSendReaction={canSendReaction}
|
canSendReaction={canSendReaction}
|
||||||
canPinEvent={canPinEvent}
|
canPinEvent={canPinEvent}
|
||||||
imagePackRooms={imagePackRooms}
|
imagePackRooms={imagePackRooms}
|
||||||
@@ -1468,6 +1470,57 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
</Event>
|
</Event>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
[StateEvent.GroupCallMemberPrefix]: (mEventId, mEvent, item) => {
|
||||||
|
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||||
|
const senderId = mEvent.getSender() ?? '';
|
||||||
|
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||||
|
|
||||||
|
const content = mEvent.getContent<SessionMembershipData>();
|
||||||
|
const prevContent = mEvent.getPrevContent();
|
||||||
|
|
||||||
|
const callJoined = content.application;
|
||||||
|
if (callJoined && 'application' in prevContent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeJSX = (
|
||||||
|
<Time
|
||||||
|
ts={mEvent.getTs()}
|
||||||
|
compact={messageLayout === MessageLayout.Compact}
|
||||||
|
hour24Clock={hour24Clock}
|
||||||
|
dateFormatString={dateFormatString}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Event
|
||||||
|
key={mEvent.getId()}
|
||||||
|
data-message-item={item}
|
||||||
|
data-message-id={mEventId}
|
||||||
|
room={room}
|
||||||
|
mEvent={mEvent}
|
||||||
|
highlight={highlighted}
|
||||||
|
messageSpacing={messageSpacing}
|
||||||
|
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||||
|
hideReadReceipts={hideActivity}
|
||||||
|
showDeveloperTools={showDeveloperTools}
|
||||||
|
>
|
||||||
|
<EventContent
|
||||||
|
messageLayout={messageLayout}
|
||||||
|
time={timeJSX}
|
||||||
|
iconSrc={callJoined ? Icons.Phone : Icons.PhoneDown}
|
||||||
|
content={
|
||||||
|
<Box grow="Yes" direction="Column">
|
||||||
|
<Text size="T300" priority="300">
|
||||||
|
<b>{senderName}</b>
|
||||||
|
{callJoined ? ' joined the call' : ' ended the call'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Event>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
(mEventId, mEvent, item) => {
|
(mEventId, mEvent, item) => {
|
||||||
if (!showHiddenEvents) return null;
|
if (!showHiddenEvents) return null;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useRef } from 'react';
|
import React, { useCallback, useRef } from 'react';
|
||||||
import { Box, Text, config } from 'folds';
|
import { Box, Text, config } from 'folds';
|
||||||
import { EventType, Room } from 'matrix-js-sdk';
|
import { EventType } from 'matrix-js-sdk';
|
||||||
import { ReactEditor } from 'slate-react';
|
import { ReactEditor } from 'slate-react';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import { useStateEvent } from '../../hooks/useStateEvent';
|
import { useStateEvent } from '../../hooks/useStateEvent';
|
||||||
@@ -15,13 +15,13 @@ import { RoomTombstone } from './RoomTombstone';
|
|||||||
import { RoomInput } from './RoomInput';
|
import { RoomInput } from './RoomInput';
|
||||||
import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing';
|
import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing';
|
||||||
import { Page } from '../../components/page';
|
import { Page } from '../../components/page';
|
||||||
import { RoomViewHeader } from './RoomViewHeader';
|
|
||||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||||
import { editableActiveElement } from '../../utils/dom';
|
import { editableActiveElement } from '../../utils/dom';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||||
|
import { useRoom } from '../../hooks/useRoom';
|
||||||
|
|
||||||
const FN_KEYS_REGEX = /^F\d+$/;
|
const FN_KEYS_REGEX = /^F\d+$/;
|
||||||
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
||||||
@@ -30,10 +30,8 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// do not focus on F keys
|
|
||||||
if (FN_KEYS_REGEX.test(code)) return false;
|
if (FN_KEYS_REGEX.test(code)) return false;
|
||||||
|
|
||||||
// do not focus on numlock/scroll lock
|
|
||||||
if (
|
if (
|
||||||
code.startsWith('OS') ||
|
code.startsWith('OS') ||
|
||||||
code.startsWith('Meta') ||
|
code.startsWith('Meta') ||
|
||||||
@@ -56,12 +54,13 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
export function RoomView({ eventId }: { eventId?: string }) {
|
||||||
const roomInputRef = useRef<HTMLDivElement>(null);
|
const roomInputRef = useRef<HTMLDivElement>(null);
|
||||||
const roomViewRef = useRef<HTMLDivElement>(null);
|
const roomViewRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||||
|
|
||||||
|
const room = useRoom();
|
||||||
const { roomId } = room;
|
const { roomId } = room;
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
|
|
||||||
@@ -93,7 +92,6 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Page ref={roomViewRef}>
|
<Page ref={roomViewRef}>
|
||||||
<RoomViewHeader />
|
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
<RoomTimeline
|
<RoomTimeline
|
||||||
key={roomId}
|
key={roomId}
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { JoinRule, Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
|
|
||||||
import { useStateEvent } from '../../hooks/useStateEvent';
|
import { useStateEvent } from '../../hooks/useStateEvent';
|
||||||
import { PageHeader } from '../../components/page';
|
import { PageHeader } from '../../components/page';
|
||||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||||
@@ -33,7 +31,7 @@ import { UseStateProvider } from '../../components/UseStateProvider';
|
|||||||
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
||||||
import { StateEvent } from '../../../types/matrix/room';
|
import { StateEvent } from '../../../types/matrix/room';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useRoom } from '../../hooks/useRoom';
|
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
import { useSpaceOptionally } from '../../hooks/useSpace';
|
import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||||
@@ -48,7 +46,6 @@ import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
|||||||
import { copyToClipboard } from '../../utils/dom';
|
import { copyToClipboard } from '../../utils/dom';
|
||||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
||||||
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
|
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
|
||||||
import { mDirectAtom } from '../../state/mDirectList';
|
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
import { getMatrixToRoom } from '../../plugins/matrix-to';
|
import { getMatrixToRoom } from '../../plugins/matrix-to';
|
||||||
@@ -69,6 +66,8 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
|||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||||
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||||
|
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||||
|
import { RoomSettingsPage } from '../../state/roomSettings';
|
||||||
|
|
||||||
type RoomMenuProps = {
|
type RoomMenuProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
@@ -254,7 +253,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export function RoomViewHeader() {
|
export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
@@ -263,12 +262,12 @@ export function RoomViewHeader() {
|
|||||||
const space = useSpaceOptionally();
|
const space = useSpaceOptionally();
|
||||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||||
const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>();
|
const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>();
|
||||||
const mDirects = useAtomValue(mDirectAtom);
|
const direct = useIsDirectRoom();
|
||||||
|
|
||||||
const pinnedEvents = useRoomPinnedEvents(room);
|
const pinnedEvents = useRoomPinnedEvents(room);
|
||||||
const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption);
|
const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption);
|
||||||
const ecryptedRoom = !!encryptionEvent;
|
const encryptedRoom = !!encryptionEvent;
|
||||||
const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
|
const avatarMxc = useRoomAvatar(room, direct);
|
||||||
const name = useRoomName(room);
|
const name = useRoomName(room);
|
||||||
const topic = useRoomTopic(room);
|
const topic = useRoomTopic(room);
|
||||||
const avatarUrl = avatarMxc
|
const avatarUrl = avatarMxc
|
||||||
@@ -295,14 +294,27 @@ export function RoomViewHeader() {
|
|||||||
setPinMenuAnchor(evt.currentTarget.getBoundingClientRect());
|
setPinMenuAnchor(evt.currentTarget.getBoundingClientRect());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openSettings = useOpenRoomSettings();
|
||||||
|
const parentSpace = useSpaceOptionally();
|
||||||
|
const handleMemberToggle = () => {
|
||||||
|
if (callView) {
|
||||||
|
openSettings(room.roomId, parentSpace?.roomId, RoomSettingsPage.MembersPage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPeopleDrawer(!peopleDrawer);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader balance={screenSize === ScreenSize.Mobile}>
|
<PageHeader
|
||||||
|
className={ContainerColor({ variant: 'Surface' })}
|
||||||
|
balance={screenSize === ScreenSize.Mobile}
|
||||||
|
>
|
||||||
<Box grow="Yes" gap="300">
|
<Box grow="Yes" gap="300">
|
||||||
{screenSize === ScreenSize.Mobile && (
|
{screenSize === ScreenSize.Mobile && (
|
||||||
<BackRouteHandler>
|
<BackRouteHandler>
|
||||||
{(onBack) => (
|
{(onBack) => (
|
||||||
<Box shrink="No" alignItems="Center">
|
<Box shrink="No" alignItems="Center">
|
||||||
<IconButton onClick={onBack}>
|
<IconButton fill="None" onClick={onBack}>
|
||||||
<Icon src={Icons.ArrowLeft} />
|
<Icon src={Icons.ArrowLeft} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -317,11 +329,7 @@ export function RoomViewHeader() {
|
|||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
alt={name}
|
alt={name}
|
||||||
renderFallback={() => (
|
renderFallback={() => (
|
||||||
<RoomIcon
|
<RoomIcon size="200" joinRule={room.getJoinRule()} roomType={room.getType()} />
|
||||||
size="200"
|
|
||||||
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
@@ -369,8 +377,9 @@ export function RoomViewHeader() {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
{!ecryptedRoom && (
|
{!encryptedRoom && (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Bottom"
|
position="Bottom"
|
||||||
offset={4}
|
offset={4}
|
||||||
@@ -381,7 +390,7 @@ export function RoomViewHeader() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(triggerRef) => (
|
{(triggerRef) => (
|
||||||
<IconButton ref={triggerRef} onClick={handleSearchClick}>
|
<IconButton fill="None" ref={triggerRef} onClick={handleSearchClick}>
|
||||||
<Icon size="400" src={Icons.Search} />
|
<Icon size="400" src={Icons.Search} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
@@ -398,6 +407,7 @@ export function RoomViewHeader() {
|
|||||||
>
|
>
|
||||||
{(triggerRef) => (
|
{(triggerRef) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
fill="None"
|
||||||
style={{ position: 'relative' }}
|
style={{ position: 'relative' }}
|
||||||
onClick={handleOpenPinMenu}
|
onClick={handleOpenPinMenu}
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
@@ -443,23 +453,29 @@ export function RoomViewHeader() {
|
|||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{screenSize === ScreenSize.Desktop && (
|
{screenSize === ScreenSize.Desktop && (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Bottom"
|
position="Bottom"
|
||||||
offset={4}
|
offset={4}
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Text>{peopleDrawer ? 'Hide Members' : 'Show Members'}</Text>
|
{callView ? (
|
||||||
|
<Text>Members</Text>
|
||||||
|
) : (
|
||||||
|
<Text>{peopleDrawer ? 'Hide Members' : 'Show Members'}</Text>
|
||||||
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(triggerRef) => (
|
{(triggerRef) => (
|
||||||
<IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}>
|
<IconButton fill="None" ref={triggerRef} onClick={handleMemberToggle}>
|
||||||
<Icon size="400" src={Icons.User} />
|
<Icon size="400" src={Icons.User} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Bottom"
|
position="Bottom"
|
||||||
align="End"
|
align="End"
|
||||||
@@ -471,7 +487,12 @@ export function RoomViewHeader() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(triggerRef) => (
|
{(triggerRef) => (
|
||||||
<IconButton onClick={handleOpenMenu} ref={triggerRef} aria-pressed={!!menuAnchor}>
|
<IconButton
|
||||||
|
fill="None"
|
||||||
|
onClick={handleOpenMenu}
|
||||||
|
ref={triggerRef}
|
||||||
|
aria-pressed={!!menuAnchor}
|
||||||
|
>
|
||||||
<Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} />
|
<Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -327,11 +327,9 @@ export const MessageCopyLinkItem = as<
|
|||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
|
||||||
const eventId = mEvent.getId();
|
const eventId = mEvent.getId();
|
||||||
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
|
|
||||||
if (!eventId) return;
|
if (!eventId) return;
|
||||||
copyToClipboard(getMatrixToRoomEvent(roomIdOrAlias, eventId, viaServers));
|
copyToClipboard(getMatrixToRoomEvent(room.roomId, eventId, getViaServers(room)));
|
||||||
onClose?.();
|
onClose?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ export function Search({ requestClose }: SearchProps) {
|
|||||||
gap="100"
|
gap="100"
|
||||||
>
|
>
|
||||||
<Text size="H6" align="Center">
|
<Text size="H6" align="Center">
|
||||||
{result ? 'No Match Found' : `No Rooms'}`}
|
{result ? 'No Match Found' : 'No Rooms'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="T200" align="Center">
|
<Text size="T200" align="Center">
|
||||||
{result
|
{result
|
||||||
@@ -373,7 +373,7 @@ export function Search({ requestClose }: SearchProps) {
|
|||||||
<RoomIcon
|
<RoomIcon
|
||||||
size="100"
|
size="100"
|
||||||
joinRule={room.getJoinRule()}
|
joinRule={room.getJoinRule()}
|
||||||
space={room.isSpaceRoom()}
|
roomType={room.getType()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function About({ requestClose }: AboutProps) {
|
|||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Box gap="100" alignItems="End">
|
<Box gap="100" alignItems="End">
|
||||||
<Text size="H3">Cinny</Text>
|
<Text size="H3">Cinny</Text>
|
||||||
<Text size="T200">v4.10.2</Text>
|
<Text size="T200">v4.11.1</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Text>Yet another matrix client.</Text>
|
<Text>Yet another matrix client.</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export function SpaceSettings({ initialPage, requestClose }: SpaceSettingsProps)
|
|||||||
alt={roomName}
|
alt={roomName}
|
||||||
renderFallback={() => (
|
renderFallback={() => (
|
||||||
<RoomIcon
|
<RoomIcon
|
||||||
space
|
roomType={room.getType()}
|
||||||
size="50"
|
size="50"
|
||||||
joinRule={joinRuleContent?.join_rule ?? JoinRule.Invite}
|
joinRule={joinRuleContent?.join_rule ?? JoinRule.Invite}
|
||||||
filled
|
filled
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function General({ requestClose }: GeneralProps) {
|
|||||||
<RoomLocalAddresses permissions={permissions} />
|
<RoomLocalAddresses permissions={permissions} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">Advance Options</Text>
|
<Text size="L400">Advanced Options</Text>
|
||||||
<RoomUpgrade permissions={permissions} requestClose={requestClose} />
|
<RoomUpgrade permissions={permissions} requestClose={requestClose} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user