Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 444b2feb9e | |||
| f35aa384e5 | |||
| b5fd41f862 | |||
| 3d4c91c969 | |||
| 38cc6e6f3a | |||
| 12bcbc2e78 | |||
| e44ca92422 | |||
| 174b315278 | |||
| d73428ee3d | |||
| f2c5a595b9 | |||
| a6a3ac3b24 | |||
| 67c6785bf3 | |||
| d36938e1fd | |||
| 1914606895 | |||
| 19096c3543 | |||
| 737cc09fea | |||
| 154f234d0c |
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"defaultHomeserver": 0,
|
|
||||||
"homeserverList": [
|
|
||||||
"matrix.lotusguild.org"
|
|
||||||
],
|
|
||||||
"allowCustomHomeservers": false,
|
|
||||||
"featuredCommunities": {
|
|
||||||
"openAsDefault": false,
|
|
||||||
"spaces": [],
|
|
||||||
"rooms": [],
|
|
||||||
"servers": []
|
|
||||||
},
|
|
||||||
"hashRouter": {
|
|
||||||
"enabled": false,
|
|
||||||
"basename": "/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"defaultHomeserver": 0,
|
|
||||||
"homeserverList": [
|
|
||||||
"matrix.lotusguild.org"
|
|
||||||
],
|
|
||||||
"allowCustomHomeservers": false,
|
|
||||||
"featuredCommunities": {
|
|
||||||
"openAsDefault": false,
|
|
||||||
"spaces": [],
|
|
||||||
"rooms": [],
|
|
||||||
"servers": []
|
|
||||||
},
|
|
||||||
"hashRouter": {
|
|
||||||
"enabled": false,
|
|
||||||
"basename": "/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
labels: ["needs-confirmation"]
|
|
||||||
body:
|
|
||||||
- type: markdown #add faqs in future
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> Please read through [the Discussion rules](https://github.com/cinnyapp/cinny/discussions/2653) and check for both existing [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "# Issue Details"
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Issue Description
|
|
||||||
description: |
|
|
||||||
Provide a detailed description of the issue. Include relevant information, such as:
|
|
||||||
- The feature or configuration option you encounter the issue with.
|
|
||||||
- Screenshots, screen recordings, or other supporting media (as needed).
|
|
||||||
- If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description.
|
|
||||||
placeholder: |
|
|
||||||
When I try to send a message in a room, the message doesn't appear in the timeline.
|
|
||||||
OR
|
|
||||||
The application crashes when I click on the settings button.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Expected Behavior
|
|
||||||
description: |
|
|
||||||
Describe how you expect Cinny to behave in this situation.
|
|
||||||
placeholder: |
|
|
||||||
I expected the message to appear in the room timeline immediately after sending.
|
|
||||||
OR
|
|
||||||
The settings panel should open smoothly without any crashes.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Actual Behavior
|
|
||||||
description: |
|
|
||||||
Describe how Cinny actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically.
|
|
||||||
placeholder: |
|
|
||||||
The application freezes for 3 seconds and then shows a white screen.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Reproduction Steps
|
|
||||||
description: |
|
|
||||||
Provide a detailed set of step-by-step instructions for reproducing this issue.
|
|
||||||
placeholder: |
|
|
||||||
1. Open Cinny and log in to my account
|
|
||||||
2. Navigate to the #general room
|
|
||||||
3. Type a message in the message box
|
|
||||||
4. Press Enter to send
|
|
||||||
5. Notice that the message doesn't appear in the timeline
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: 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
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
name: 🐞 Bug Report
|
||||||
|
description: Report a bug
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## First of all
|
||||||
|
1. Please search for [existing issues](https://github.com/ajbura/cinny/issues?q=is%3Aissue) about this problem first.
|
||||||
|
2. Make sure Cinny is up to date.
|
||||||
|
3. Make sure it's an issue with Cinny and not something else you are using.
|
||||||
|
4. Remember to be friendly.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: A clear description of what the bug is. Include screenshots if applicable.
|
||||||
|
placeholder: Bug description
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction
|
||||||
|
attributes:
|
||||||
|
label: Reproduction
|
||||||
|
description: Steps to reproduce the behavior.
|
||||||
|
placeholder: |
|
||||||
|
1. Go to ...
|
||||||
|
2. Click on ...
|
||||||
|
3. See error
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: A clear description of what you expected to happen.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: info
|
||||||
|
attributes:
|
||||||
|
label: Platform and versions
|
||||||
|
description: "Provide OS, browser and Cinny version with your Homeserver."
|
||||||
|
placeholder: |
|
||||||
|
1. OS: [e.g. Windows 10, MacOS]
|
||||||
|
2. Browser: [e.g. chrome 99.5, firefox 97.2]
|
||||||
|
3. Cinny version: [e.g. 1.8.1 (app.cinny.in)]
|
||||||
|
4. Matrix homeserver: [e.g. matrix.org]
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context about the problem here.
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Features, Bug Reports, Questions
|
- name: 💬 Matrix Chat
|
||||||
url: https://github.com/cinnyapp/cinny/discussions/new/choose
|
url: https://matrix.to/#/#cinny:matrix.org
|
||||||
about: Our preferred starting point if you have any questions or suggestions about features or behavior.
|
about: Ask questions and talk to other Cinny users and the maintainers
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: 💡 Feature Request
|
||||||
|
description: Suggest an idea
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Describe the problem
|
||||||
|
description: A clear description of the problem this feature would solve
|
||||||
|
placeholder: "I'm always frustrated when..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: "Describe the solution you'd like"
|
||||||
|
description: A clear description of what change you would like
|
||||||
|
placeholder: "I would like to..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives considered
|
||||||
|
description: "Any alternative solutions you've considered"
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context about the problem here.
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
name: Pre-Discussed and Approved Topics
|
|
||||||
about: |-
|
|
||||||
Only for topics already discussed and approved in the GitHub Discussions section.
|
|
||||||
---
|
|
||||||
|
|
||||||
**DO NOT OPEN A NEW ISSUE. PLEASE USE THE DISCUSSIONS SECTION.**
|
|
||||||
|
|
||||||
**I DIDN'T READ THE ABOVE LINE. PLEASE CLOSE THIS ISSUE.**
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!-- 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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Reporting a Vulnerability
|
||||||
|
|
||||||
|
**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**
|
||||||
@@ -1,22 +1,10 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": ["config:recommended", ":dependencyDashboardApproval"],
|
||||||
"config:recommended",
|
|
||||||
":dependencyDashboardApproval",
|
|
||||||
":semanticCommits",
|
|
||||||
"group:monorepos"
|
|
||||||
],
|
|
||||||
"labels": ["Dependencies"],
|
"labels": ["Dependencies"],
|
||||||
"rebaseWhen": "conflicted",
|
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["lockFileMaintenance"]
|
"matchUpdateTypes": ["lockFileMaintenance"]
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchPackageNames": ["slate", "slate-dom", "slate-history", "slate-react"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchPackageNames": ["linkifyjs", "linkify-react"]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lockFileMaintenance": {
|
"lockFileMaintenance": {
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ jobs:
|
|||||||
PR_NUMBER: ${{github.event.number}}
|
PR_NUMBER: ${{github.event.number}}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4.2.0
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".node-version"
|
node-version: 20.12.2
|
||||||
package-manager-cache: false
|
cache: 'npm'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Build app
|
- name: Build app
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
run: npm run build
|
run: npm run build
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4.6.2
|
||||||
with:
|
with:
|
||||||
name: preview
|
name: preview
|
||||||
path: dist
|
path: dist
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Save pr number
|
- name: Save pr number
|
||||||
run: echo ${PR_NUMBER} > ./pr.txt
|
run: echo ${PR_NUMBER} > ./pr.txt
|
||||||
- name: Upload pr number
|
- name: Upload pr number
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4.6.2
|
||||||
with:
|
with:
|
||||||
name: pr
|
name: pr
|
||||||
path: ./pr.txt
|
path: ./pr.txt
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- name: 'CLA Assistant'
|
- name: 'CLA Assistant'
|
||||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||||
# Beta Release
|
# Beta Release
|
||||||
uses: cla-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
|
uses: cla-assistant/github-action@v2.6.1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
name: Deploy PR to Netlify
|
name: Deploy PR to Netlify
|
||||||
run-name: "Deploy PR to Netlify (${{ github.event.workflow_run.head_branch }})"
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
@@ -16,22 +15,16 @@ 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@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
|
||||||
with:
|
with:
|
||||||
workflow: ${{ github.event.workflow.id }}
|
workflow: ${{ github.event.workflow.id }}
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
name: pr
|
name: pr
|
||||||
- name: Validate and output pr number
|
- name: Output pr number
|
||||||
id: pr
|
id: pr
|
||||||
run: |
|
run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT
|
||||||
PR_ID=$(<pr.txt)
|
|
||||||
if ! [[ "${PR_ID}" =~ ^[0-9]+$ ]]; then
|
|
||||||
echo "::error::pr.txt contains non-numeric content: ${PR_ID}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "id=${PR_ID}" >> "${GITHUB_OUTPUT}"
|
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
|
||||||
with:
|
with:
|
||||||
workflow: ${{ github.event.workflow.id }}
|
workflow: ${{ github.event.workflow.id }}
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
@@ -39,7 +32,7 @@ jobs:
|
|||||||
path: dist
|
path: dist
|
||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
id: netlify
|
id: netlify
|
||||||
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
|
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
|
||||||
with:
|
with:
|
||||||
publish-dir: dist
|
publish-dir: dist
|
||||||
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
|
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
|
||||||
@@ -48,16 +41,16 @@ jobs:
|
|||||||
enable-pull-request-comment: false
|
enable-pull-request-comment: false
|
||||||
enable-commit-comment: false
|
enable-commit-comment: false
|
||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN_PR }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PR_CINNY }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PR_CINNY }}
|
||||||
timeout-minutes: 1
|
timeout-minutes: 1
|
||||||
- name: Comment preview on PR
|
- name: Comment preview on PR
|
||||||
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b #v3.0.1
|
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6
|
||||||
env:
|
env:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
pr-number: ${{ steps.pr.outputs.id }}
|
pr_number: ${{ steps.pr.outputs.id }}
|
||||||
comment-tag: ${{ steps.pr.outputs.id }}
|
comment_tag: ${{ steps.pr.outputs.id }}
|
||||||
message: |
|
message: |
|
||||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||||
@@ -5,59 +5,15 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'Dockerfile'
|
- 'Dockerfile'
|
||||||
- '.github/workflows/docker-pr.yml'
|
- '.github/workflows/docker-pr.yml'
|
||||||
- '.github/workflows/prod-deploy.yml'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-build:
|
docker-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4.2.0
|
||||||
|
- name: Build Docker image
|
||||||
- name: Set up QEMU
|
uses: docker/build-push-action@v6.18.0
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
|
||||||
|
|
||||||
- name: Login to Docker Hub #Do not update this action from a outside PR
|
|
||||||
if: github.event.pull_request.head.repo.fork == false
|
|
||||||
uses: docker/login-action@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
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4.2.0
|
||||||
- name: NPM Lockfile Changes
|
- name: NPM Lockfile Changes
|
||||||
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891 # v1.0.0
|
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# Optional inputs, can be deleted safely if you are happy with default values.
|
# Optional inputs, can be deleted safely if you are happy with default values.
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4.2.0
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".node-version"
|
node-version: 20.12.2
|
||||||
package-manager-cache: false
|
cache: 'npm'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Build app
|
- name: Build app
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
run: npm run build
|
run: npm run build
|
||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
|
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
|
||||||
with:
|
with:
|
||||||
publish-dir: dist
|
publish-dir: dist
|
||||||
deploy-message: 'Dev deploy ${{ github.sha }}'
|
deploy-message: 'Dev deploy ${{ github.sha }}'
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
name: Check PR title
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- edited
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -1,48 +1,32 @@
|
|||||||
name: Production deploy
|
name: Production deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
release:
|
||||||
|
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4.2.0
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".node-version"
|
node-version: 20.12.2
|
||||||
package-manager-cache: false
|
cache: 'npm'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: 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 # v3.0.0
|
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
|
||||||
with:
|
with:
|
||||||
publish-dir: dist
|
publish-dir: dist
|
||||||
deploy-message: 'Prod deploy ${{ steps.vars.outputs.tag }}'
|
deploy-message: 'Prod deploy ${{ github.ref_name }}'
|
||||||
enable-commit-comment: false
|
enable-commit-comment: false
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
production-deploy: true
|
production-deploy: true
|
||||||
@@ -52,6 +36,9 @@ 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
|
||||||
@@ -65,54 +52,45 @@ jobs:
|
|||||||
gpg --export | xxd -p
|
gpg --export | xxd -p
|
||||||
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||||
- name: Upload tagged release
|
- name: Upload tagged release
|
||||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4.2.0
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@v3.6.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@v3.11.1
|
||||||
- name: Login to Docker Hub #Do not update this action from a outside PR
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@v3.5.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Login to the Github Container registry #Do not update this action from a outside PR
|
- name: Login to the Container registry
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Extract metadata (tags, labels) for Docker, GHCR
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
uses: docker/metadata-action@v5.8.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_USERNAME }}/cinny
|
${{ secrets.DOCKER_USERNAME }}/cinny
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
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@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
uses: docker/build-push-action@v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# These are commented until we enable lint and typecheck
|
|
||||||
# npx tsc -p tsconfig.json --noEmit
|
|
||||||
# npx lint-staged
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
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. You will also be asked to [sign the CLA](https://github.com/cinnyapp/cla) upon submiting your pull request.
|
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
|
||||||
|
|
||||||
**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:24.13.1-alpine AS builder
|
FROM node:20.12.2-alpine3.18 as builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ RUN npm run build
|
|||||||
|
|
||||||
|
|
||||||
## App
|
## App
|
||||||
FROM nginx:1.29.8-alpine
|
FROM nginx:1.29.1-alpine
|
||||||
|
|
||||||
COPY --from=builder /src/dist /app
|
COPY --from=builder /src/dist /app
|
||||||
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
|
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|||||||
@@ -1,65 +1,111 @@
|
|||||||
# Lotus Chat
|
# Cinny
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/ajbura/cinny/releases">
|
||||||
|
<img alt="GitHub release downloads" src="https://img.shields.io/github/downloads/ajbura/cinny/total?logo=github&style=social"></a>
|
||||||
|
<a href="https://hub.docker.com/r/ajbura/cinny">
|
||||||
|
<img alt="DockerHub downloads" src="https://img.shields.io/docker/pulls/ajbura/cinny?logo=docker&style=social"></a>
|
||||||
|
<a href="https://fosstodon.org/@cinnyapp">
|
||||||
|
<img alt="Follow on Mastodon" src="https://img.shields.io/mastodon/follow/106845779685925461?domain=https%3A%2F%2Ffosstodon.org&logo=mastodon&style=social"></a>
|
||||||
|
<a href="https://twitter.com/intent/follow?screen_name=cinnyapp">
|
||||||
|
<img alt="Follow on Twitter" src="https://img.shields.io/twitter/follow/cinnyapp?logo=twitter&style=social"></a>
|
||||||
|
<a href="https://cinny.in/#sponsor">
|
||||||
|
<img alt="Sponsor Cinny" src="https://img.shields.io/opencollective/all/cinny?logo=opencollective&style=social"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
A Matrix client for [Lotus Guild](https://lotusguild.org) — forked from [Cinny](https://github.com/cinnyapp/cinny) v4.11.1.
|
A Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have an instant messaging application that is easy on people and has a modern touch.
|
||||||
|
- [Roadmap](https://github.com/orgs/cinnyapp/projects/1)
|
||||||
|
- [Contributing](./CONTRIBUTING.md)
|
||||||
|
|
||||||
Deployed at [chat.lotusguild.org](https://chat.lotusguild.org).
|
<img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
|
||||||
|
|
||||||
---
|
## Getting started
|
||||||
|
The web app is available at [app.cinny.in](https://app.cinny.in/) and gets updated on each new release. The `dev` branch is continuously deployed at [dev.cinny.in](https://dev.cinny.in) but keep in mind that it could have things broken.
|
||||||
|
|
||||||
## Changes from upstream Cinny
|
You can also download our desktop app from the [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
|
||||||
|
|
||||||
### Branding & Identity
|
## Self-hosting
|
||||||
|
To host Cinny on your own, simply download the tarball from [GitHub releases](https://github.com/cinnyapp/cinny/releases/latest), and serve the files from `dist/` using your preferred webserver. Alternatively, you can just pull the docker image from [DockerHub](https://hub.docker.com/r/ajbura/cinny) or [GitHub Container Registry](https://github.com/cinnyapp/cinny/pkgs/container/cinny).
|
||||||
|
|
||||||
- Package renamed to `lotus-chat`, description updated to "Lotus Chat — Matrix client for Lotus Guild"
|
* The default homeservers and explore pages are defined in [`config.json`](config.json).
|
||||||
- App title changed from "Cinny" to "Lotus Chat" throughout
|
|
||||||
- Favicon, PWA icons, and all icon sizes (57×57 → 180×180 Apple touch icons) replaced with Lotus.png variants
|
|
||||||
- Logo in About dialog and Auth page replaced with official Lotus.png
|
|
||||||
- Auth footer rewritten: shows dynamic version from `package.json`, links to lotusguild.org, chat.lotusguild.org, and matrix.lotusguild.org
|
|
||||||
- Welcome page tagline changed from "Yet another matrix client" to "A Matrix client for Lotus Guild"
|
|
||||||
- Encryption key export filename changed from `cinny-keys.txt` to `lotus-keys.txt`
|
|
||||||
- `manifest.json` updated with Lotus name, description, and branding colors
|
|
||||||
|
|
||||||
### LotusGuild Terminal Design System (TDS) v1.2
|
* You need to set up redirects to serve the assests. Example configurations; [netlify](netlify.toml), [nginx](contrib/nginx/cinny.domain.tld.conf), [caddy](contrib/caddy/caddyfile).
|
||||||
|
* If you have trouble configuring redirects you can [enable hash routing](config.json#L35) — the url in the browser will have a `/#/` between the domain and open channel (ie. `app.cinny.in/#/home/` instead of `app.cinny.in/home/`) but you won't have to configure your webserver.
|
||||||
|
|
||||||
A full custom theme engine layered on top of Cinny's vanilla-extract theming:
|
* To deploy on subdirectory, you need to rebuild the app youself after updating the `base` path in [`build.config.ts`](build.config.ts).
|
||||||
|
* For example, if you want to deploy on `https://cinny.in/app`, then set `base: '/app'`.
|
||||||
|
|
||||||
**Dark mode** (`LotusTerminalTheme`):
|
<details><summary><b>PGP Public Key to verify tarball</b></summary>
|
||||||
- CRT terminal aesthetic: scanline overlay, vignette, phosphor glow
|
|
||||||
- Palette: bg `#030508`, orange `#FF6B00`, cyan `#00D4FF`, green `#00FF88`, text `#c4d9ee`
|
|
||||||
- Monospace font stack, terminal-style scrollbars
|
|
||||||
- Custom hex-grid and circuit-board CSS background patterns
|
|
||||||
- Matrix-style boot messages on the welcome page
|
|
||||||
- CSS variables: `--lt-*` family covering colors, glow effects, borders, animations
|
|
||||||
|
|
||||||
**Light mode** (`LotusTerminalLightTheme`):
|
```
|
||||||
- Full light palette: bg `#edf0f5`, orange `#c44e00`, cyan `#0062b8`, green `#006d35`, text `#111827`
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
- No CRT effects (scanlines, vignette disabled)
|
|
||||||
- Light-mode scrollbars, adjusted code block colors, semantic color overrides
|
|
||||||
- Scoped to `html[data-theme="light"] body.lotusTerminalBodyClass`
|
|
||||||
- `ThemeManager.tsx` sets `data-theme` attribute based on active theme kind
|
|
||||||
|
|
||||||
**Chat Backgrounds** (18+ custom patterns, all TDS-aware):
|
mQGNBGJw/g0BDAC8qQeLqDMzYzfPyOmRlHVEoguVTo+eo1aVdQH2X7OELdjjBlyj
|
||||||
- Blueprint grid, carbon fiber, starfield, topographic contours, herringbone, crosshatch
|
6d6c1adv/uF2g83NNMoQY7GEeHjRnXE4m8kYSaarb840pxrYUagDc0dAbJOGaCBY
|
||||||
- Chevron, polka dots, triangles, plaid
|
FKTo7U1Kvg0vdiaRuus0pvc1NVdXSxRNQbFXBSwduD+zn66TI3HfcEHNN62FG1cE
|
||||||
- All patterns use CSS custom properties — adapt to both TDS dark and light themes
|
K1jWDwLAU0P3kKmj8+CAc3h9ZklPu0k/+t5bf/LJkvdBJAUzGZpehbPL5f3u3BZ0
|
||||||
- Settings toggle for showing per-message sender profiles
|
leZLIrR8uV7PiV5jKFahxlKR5KQHld8qQm+qVhYbUzpuMBGmh419I6UvTzxuRcvU
|
||||||
|
Frn9ttCEzV55Y+so4X2e4ZnB+5gOnNw+ecifGVdj/+UyWnqvqqDvLrEjjK890nLb
|
||||||
|
Pil4siecNMEpiwAN6WSmKpWaCwQAHEGDVeZCc/kT0iYfj5FBcsTVqWiO6eaxkUlm
|
||||||
|
jnulqWqRrlB8CJQQvih/g//uSEBdzIibo+ro+3Jpe120U/XVUH62i9HoRQEm6ADG
|
||||||
|
4zS5hIq4xyA8fL8AEQEAAbQdQ2lubnlBcHAgPGNpbm55YXBwQGdtYWlsLmNvbT6J
|
||||||
|
AdQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQSRri2MHidaaZv+
|
||||||
|
vvuUMwx6UK/M8wUCZqEDwAUJFvwIswAKCRCUMwx6UK/M877qC/4lxXOQIoWnLLkK
|
||||||
|
YiRCTkGsH6NdxgeYr6wpXT4xuQ45ZxCytwHpOGQmO/5up5961TxWW8D1frRIJHjj
|
||||||
|
AZGoRCL3EKEuY8nt3D99fpf3DvZrs1uoVAhiyn737hRlZAg+QsJheeGCmdSJ0hX5
|
||||||
|
Yud8SE+9zxLS1+CEjMrsUd/RGre/phme+wNXfaHfREAC9ewolgVChPIbMxG2f+vs
|
||||||
|
K8Xv52BFng7ta9fgsl1XuOjpuaSbQv6g+4ONk/lxKF0SmnhEGM3dmIYPONxW47Yf
|
||||||
|
atnIjRra/YhPTNwrNBGMmG4IFKaOsMbjW/eakjWTWOVKKJNBMoDdRcYYWIMCpLy8
|
||||||
|
AQUrMtQEsHSnqCwrw818S5A6rrhcfVGk36RGm0nOy6LS5g5jmqaYsvbCcBGY9B2c
|
||||||
|
SUAVNm17oo7TtEajk8hcSXoZod1t++pyjcVKEmSn3nFK7v5m3V+cPhNTxZMK459P
|
||||||
|
3x1Ucqj/kTqrxKw6s2Uknuk0ajmw0ljV+BQwgL6maguo9BKgCNW5AY0EYnD+DQEM
|
||||||
|
ANOu/d6ZMF8bW+Df9RDCUQKytbaZfa+ZbIHBus7whCD/SQMOhPKntv3HX7SmMCs+
|
||||||
|
5i27kJMu4YN623JCS7hdCoXVO1R5kXCEcneW/rPBMDutaM472YvIWMIqK9Wwl5+0
|
||||||
|
Piu2N+uTkKhe9uS2u7eN+Khef3d7xfjGRxoppM+xI9dZO+jhYiy8LuC0oBohTjJq
|
||||||
|
QPqfGDpowBwRkkOsGz/XVcesJ1Pzg4bKivTS9kZjZSyT9RRSY8As0sVUN57AwYul
|
||||||
|
s1+eh00n/tVpi2Jj9pCm7S0csSXvXj8v2OTdK1jt4YjpzR0/rwh4+/xlOjDjZEqH
|
||||||
|
vMPhpzpbgnwkxZ3X8BFne9dJ3maC5zQ3LAeCP5m1W0hXzagYhfyjo74slJgD1O8c
|
||||||
|
LDf2Oxc5MyM8Y/UK497zfqSPfgT3NhQmhHzk83DjXw3I6Z3A3U+Jp61w0eBRI1nx
|
||||||
|
H1UIG+gldcAKUTcfwL0lghoT3nmi9JAbvek0Smhz00Bbo8/dx8vwQRxDUxlt7Exx
|
||||||
|
NwARAQABiQG8BBgBCAAmAhsMFiEEka4tjB4nWmmb/r77lDMMelCvzPMFAmahA9IF
|
||||||
|
CRb8CMUACgkQlDMMelCvzPPQgQv/d5/z+fxgKqgfhQX+V49X4WgTVxZ/CzztDoJ1
|
||||||
|
XAq1dzTNEy8AFguXIo6eVXPSpMxec7ZreN3+UPQBnCf3eR5YxWNYOYKmk0G4E8D2
|
||||||
|
KGUJept7TSA42/8N2ov6tToXFg4CgzKZj0fYLwgutly7K8eiWmSU6ptaO8aEQBHB
|
||||||
|
gTGIOO3h6vJMGVycmoeRnHjv4wV84YWSVFSoJ7cY0he4Z9UznJBbE/KHZjrkXsPo
|
||||||
|
N+Gg5lDuOP5xjKzM5SogV9lhxBAhMWAg3URUF15yruZBiA8uV1FOK8sal/9C1G7V
|
||||||
|
M6ygA6uOZqXlZtcdA94RoSsW2pZ9eLVPsxz2B3Zko7tu11MpNP/wYmfGTI3KxZBj
|
||||||
|
n/eodvwjJSgHpGOFSmbNzvPJo3to5nNlp7wH1KxIMc6Uuu9hgfDfwkFZgV2bnFIa
|
||||||
|
Q6gyF548Ub48z7Dz83+WwLgbX19ve4oZx+dqSdczP6ILHRQomtrzrkkP2LU52oI5
|
||||||
|
mxFo+ioe/ABCufSmyqFye0psX3Sp
|
||||||
|
=WtqZ
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
### Voice / Video Call Improvements
|
## Local development
|
||||||
|
> [!TIP]
|
||||||
|
> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Iron LTS (v20).
|
||||||
|
|
||||||
- **Element Call 0.19.3**: Upgraded from 0.16.3. Dist copied to `public/element-call/` by vite at build time.
|
Execute the following commands to start a development server:
|
||||||
- **Camera default OFF**: Camera no longer persists across sessions via localStorage. Always starts disabled. Optional `cameraOnJoin` setting for explicit opt-in.
|
```sh
|
||||||
- **Deafen button**: Tooltip corrected to "Deafen" / "Undeafen" (was "Turn Off Sound" / "Turn On Sound")
|
npm ci # Installs all dependencies
|
||||||
- **Screenshare confirmation**: A confirm dialog appears before screenshare is broadcast to call participants
|
npm start # Serve a development version
|
||||||
- **Auto-revert spotlight on screenshare**: When someone starts screensharing, EC normally forces all participants into spotlight view. Patched in `CallControl.ts` `onControlMutation()` — detects the screenshare button going `primary` and clicks `gridButton` after 600ms to revert to grid layout. Participants choose to watch screenshare manually.
|
```
|
||||||
- **Push to Talk (PTT)**:
|
|
||||||
- Configurable keybind (default: Space) via Settings > General > Calls
|
To build the app:
|
||||||
- Mic activates on keydown, deactivates on keyup; mic muted on tab blur/focus to prevent stuck-on mic
|
```sh
|
||||||
- Visual indicator: plain folds `Chip` by default; when LotusGuild TDS is active: orange `PTT — Hold SPACE` / green `● LIVE` in JetBrains Mono
|
npm run build # Compiles the app into the dist/ directory
|
||||||
- Listens on both main window and EC iframe `contentWindow` for reliable key capture
|
```
|
||||||
- Implemented via `CallControl.setMicrophone()` public method on the widget bridge
|
|
||||||
- **Noise suppression toggle**: Settings > General > Calls — passes `noiseSuppression` URL parameter to the embedded Element Call widget
|
### Running with Docker
|
||||||
- **DM calls**: Phone button in DM room header starts a 1-on-1 call via `useCallStart(true)`. `Room.tsx` switches to CallView layout when the DM has an active call embed. Standard voice rooms were already supported; this extends call support to all direct messages.
|
This repository includes a Dockerfile, which builds the application from source and serves it with Nginx on port 80. To
|
||||||
- **Poll display**: `m.poll.start` events (both stable Matrix 1.7 `m.poll` content key and MSC3381 unstable `org.matrix.msc3381.poll.start`) render as read-only poll cards inside the standard message bubble — question and answer options shown. Registered as top-level event renderers AND inside the `EncryptedContent` callback so encrypted polls also display after decryption. "Open in Element to vote" note displayed. Implemented in `PollContent.tsx`.
|
use this locally, you can build the container like so:
|
||||||
- **Deleted message placeholder**: Redacted `m.room.message`, `m.room.encrypted`, and `m.sticker` events no longer disappear from the timeline. Instead they reach the existing `RedactedContent` component (trash icon + italic "This message has been deleted" with reason if provided), matching Element, FluffyChat, Commet, and Nheko behaviour. One-line change in the `eventRenderer` filter in `RoomTimeline.tsx`.
|
```
|
||||||
- **Incoming DM call notification**: When another user starts a call in a DM room, a fixed-position notification appears with the caller's avatar and display name, Answer and Decline buttons, and a Web Audio API double-pulse ring tone. The notification auto-dismisses after 30 seconds or when the caller leaves. Fully themed for both LotusGuild Terminal TDS (dark navy `#060c14` background, orange border, neon-green Answer button, JetBrains Mono font, pulsing indicator dot) and standard Cinny dark/light mode (CSS variable theming, folds `Button` with `variant="Success"/"Critical"`). Implemented in `useIncomingDmCall` hook (watches `MatrixRTCSessionManagerEvents.SessionStarted`, filters encrypted ≤2-member rooms, ignores self) and `IncomingCallNotification` component mounted inside `CallEmbedProvider` in `Router.tsx`.
|
docker build -t cinny:latest .
|
||||||
- **Picture-in-picture (PiP)**: When navigating away from a call room while in an active call, the call embed shrinks to a 280x158px floating window in the bottom-right corner. The PiP window is **draggable** — drag it anywhere on screen to move it out of the way. Clicking (without dragging) navigates back to the call room. Drag vs click distinguished by a 5px movement threshold; touch drag supported. Imperative style overrides on `callEmbedRef.current` via `useEffect` — a wrapper div cannot be used because `useCallEmbedPlacementSync` writes `top/left/width/height` directly onto that element.
|
```
|
||||||
|
|
||||||
|
You can then run the container you've built with a command similar to this:
|
||||||
|
```
|
||||||
|
docker run -p 8080:80 cinny:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by navigating to `http://localhost:8080`.
|
||||||
|
|||||||
@@ -1,18 +1,38 @@
|
|||||||
{
|
{
|
||||||
"defaultHomeserver": 0,
|
"defaultHomeserver": 2,
|
||||||
"homeserverList": [
|
"homeserverList": [
|
||||||
"matrix.lotusguild.org"
|
"converser.eu",
|
||||||
|
"envs.net",
|
||||||
|
"matrix.org",
|
||||||
|
"monero.social",
|
||||||
|
"mozilla.org",
|
||||||
|
"xmr.se"
|
||||||
],
|
],
|
||||||
"allowCustomHomeservers": false,
|
"allowCustomHomeservers": true,
|
||||||
|
|
||||||
"featuredCommunities": {
|
"featuredCommunities": {
|
||||||
"openAsDefault": false,
|
"openAsDefault": false,
|
||||||
"spaces": [],
|
"spaces": [
|
||||||
"rooms": [],
|
"#cinny-space:matrix.org",
|
||||||
"servers": []
|
"#community:matrix.org",
|
||||||
|
"#space:envs.net",
|
||||||
|
"#science-space:matrix.org",
|
||||||
|
"#libregaming-games:tchncs.de",
|
||||||
|
"#mathematics-on:matrix.org"
|
||||||
|
],
|
||||||
|
"rooms": [
|
||||||
|
"#cinny:matrix.org",
|
||||||
|
"#freesoftware:matrix.org",
|
||||||
|
"#pcapdroid:matrix.org",
|
||||||
|
"#gentoo:matrix.org",
|
||||||
|
"#PrivSec.dev:arcticfoxes.net",
|
||||||
|
"#disroot:aria-net.org"
|
||||||
|
],
|
||||||
|
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
|
||||||
},
|
},
|
||||||
|
|
||||||
"hashRouter": {
|
"hashRouter": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"basename": "/"
|
"basename": "/"
|
||||||
},
|
}
|
||||||
"gifApiKey": "AqqDuQwZNjYttz7Mn6ME4JH1bJIuZ5CO"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,36 +4,33 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
||||||
<title>Lotus Chat</title>
|
<title>Cinny</title>
|
||||||
<meta name="name" content="Lotus Chat" />
|
<meta name="name" content="Cinny" />
|
||||||
<meta name="author" content="Lotus Guild" />
|
<meta name="author" content="Ajay Bura" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
|
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="keywords"
|
name="keywords"
|
||||||
content="lotus chat, lotus guild, matrix, matrix client"
|
content="cinny, cinnyapp, cinnychat, matrix, matrix client, matrix.org, element"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<meta property="og:title" content="Lotus Chat" />
|
<meta property="og:title" content="Cinny" />
|
||||||
<meta property="og:url" content="https://chat.lotusguild.org" />
|
<meta property="og:url" content="https://cinny.in" />
|
||||||
<meta property="og:image" content="https://chat.lotusguild.org/public/res/android/android-chrome-192x192.png" />
|
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
|
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=VT323&display=swap" rel="stylesheet">
|
|
||||||
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="application-name" content="Lotus Chat" />
|
<meta name="application-name" content="Cinny" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Lotus Chat" />
|
<meta name="apple-mobile-web-app-title" content="Cinny" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "lotus-chat",
|
"name": "cinny",
|
||||||
"version": "4.12.1-lotus",
|
"version": "4.10.0",
|
||||||
"description": "Lotus Chat — Matrix client for Lotus Guild",
|
"description": "Yet another matrix client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -10,53 +10,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"lint": "yarn check:eslint && yarn check:prettier",
|
||||||
"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",
|
||||||
@@ -66,9 +24,6 @@
|
|||||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
|
||||||
"@fontsource/inter": "4.5.14",
|
"@fontsource/inter": "4.5.14",
|
||||||
"@giphy/js-fetch-api": "5.8.0",
|
|
||||||
"@giphy/js-types": "5.1.0",
|
|
||||||
"@giphy/react-components": "10.1.2",
|
|
||||||
"@tanstack/react-query": "5.24.1",
|
"@tanstack/react-query": "5.24.1",
|
||||||
"@tanstack/react-query-devtools": "5.24.1",
|
"@tanstack/react-query-devtools": "5.24.1",
|
||||||
"@tanstack/react-virtual": "3.2.0",
|
"@tanstack/react-virtual": "3.2.0",
|
||||||
@@ -88,7 +43,7 @@
|
|||||||
"emojibase-data": "15.3.2",
|
"emojibase-data": "15.3.2",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"focus-trap-react": "10.0.2",
|
"focus-trap-react": "10.0.2",
|
||||||
"folds": "2.6.2",
|
"folds": "2.5.0",
|
||||||
"html-dom-parser": "4.0.0",
|
"html-dom-parser": "4.0.0",
|
||||||
"html-react-parser": "4.2.0",
|
"html-react-parser": "4.2.0",
|
||||||
"i18next": "23.12.2",
|
"i18next": "23.12.2",
|
||||||
@@ -97,10 +52,9 @@
|
|||||||
"immer": "9.0.16",
|
"immer": "9.0.16",
|
||||||
"is-hotkey": "0.2.0",
|
"is-hotkey": "0.2.0",
|
||||||
"jotai": "2.6.0",
|
"jotai": "2.6.0",
|
||||||
"linkify-react": "4.3.2",
|
"linkify-react": "4.1.3",
|
||||||
"linkifyjs": "4.3.2",
|
"linkifyjs": "4.1.3",
|
||||||
"matrix-js-sdk": "38.2.0",
|
"matrix-js-sdk": "38.2.0",
|
||||||
"matrix-widget-api": "1.16.1",
|
|
||||||
"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",
|
||||||
@@ -113,22 +67,18 @@
|
|||||||
"react-google-recaptcha": "2.1.0",
|
"react-google-recaptcha": "2.1.0",
|
||||||
"react-i18next": "15.0.0",
|
"react-i18next": "15.0.0",
|
||||||
"react-range": "1.8.14",
|
"react-range": "1.8.14",
|
||||||
"react-router-dom": "6.30.3",
|
"react-router-dom": "6.20.0",
|
||||||
"sanitize-html": "2.17.4",
|
"sanitize-html": "2.12.1",
|
||||||
"slate": "0.123.0",
|
"slate": "0.112.0",
|
||||||
"slate-dom": "0.123.0",
|
"slate-dom": "0.112.2",
|
||||||
"slate-history": "0.113.1",
|
"slate-history": "0.110.3",
|
||||||
"slate-react": "0.123.0",
|
"slate-react": "0.112.1",
|
||||||
"styled-components": "6.4.1",
|
|
||||||
"ua-parser-js": "1.0.35"
|
"ua-parser-js": "1.0.35"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@element-hq/element-call-embedded": "0.19.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",
|
||||||
@@ -137,13 +87,12 @@
|
|||||||
"@types/react": "18.2.39",
|
"@types/react": "18.2.39",
|
||||||
"@types/react-dom": "18.2.17",
|
"@types/react-dom": "18.2.17",
|
||||||
"@types/react-google-recaptcha": "2.1.8",
|
"@types/react-google-recaptcha": "2.1.8",
|
||||||
"@types/sanitize-html": "2.16.1",
|
"@types/sanitize-html": "2.9.0",
|
||||||
"@types/ua-parser-js": "0.7.36",
|
"@types/ua-parser-js": "0.7.36",
|
||||||
"@typescript-eslint/eslint-plugin": "5.46.1",
|
"@typescript-eslint/eslint-plugin": "5.46.1",
|
||||||
"@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",
|
||||||
@@ -151,10 +100,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"defaultHomeserver": 0,
|
|
||||||
"homeserverList": [
|
|
||||||
"matrix.lotusguild.org"
|
|
||||||
],
|
|
||||||
"allowCustomHomeservers": false,
|
|
||||||
"featuredCommunities": {
|
|
||||||
"openAsDefault": false,
|
|
||||||
"spaces": [],
|
|
||||||
"rooms": [],
|
|
||||||
"servers": []
|
|
||||||
},
|
|
||||||
"hashRouter": {
|
|
||||||
"enabled": false,
|
|
||||||
"basename": "/"
|
|
||||||
},
|
|
||||||
"gifApiKey": "AqqDuQwZNjYttz7Mn6ME4JH1bJIuZ5CO"
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 32 KiB |
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "Lotus Chat",
|
"name": "Cinny",
|
||||||
"short_name": "Lotus Chat",
|
"short_name": "Cinny",
|
||||||
"description": "Lotus Chat \u2014 the Lotus Guild Matrix client",
|
"description": "Yet another matrix client",
|
||||||
"dir": "auto",
|
"dir": "auto",
|
||||||
"lang": "en-US",
|
"lang": "en-US",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"start_url": "./",
|
"start_url": "./",
|
||||||
"background_color": "#0a0a0a",
|
"background_color": "#fff",
|
||||||
"theme_color": "#980000",
|
"theme_color": "#fff",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "./public/android/android-chrome-36x36.png",
|
"src": "./public/android/android-chrome-36x36.png",
|
||||||
@@ -55,24 +55,5 @@
|
|||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"categories": [
|
|
||||||
"social",
|
|
||||||
"communication",
|
|
||||||
"productivity"
|
|
||||||
],
|
|
||||||
"shortcuts": [
|
|
||||||
{
|
|
||||||
"name": "New Message",
|
|
||||||
"short_name": "DM",
|
|
||||||
"description": "Open a new direct message",
|
|
||||||
"url": "/",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "res/android/android-chrome-96x96.png",
|
|
||||||
"sizes": "96x96"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,14 +1,13 @@
|
|||||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g fill="#980000" fill-opacity="0.88">
|
<g clip-path="url(#clip0_2707_1961)">
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(0,9,9)"/>
|
<path d="M10.5867 17.3522C10.0727 17.4492 9.54226 17.5 9 17.5C4.30558 17.5 0.5 13.6944 0.5 9C0.5 4.30558 4.30558 0.5 9 0.5C13.6944 0.5 17.5 4.30558 17.5 9C17.5 9.54226 17.4492 10.0727 17.3522 10.5867C16.6511 10.2123 15.8503 10 15 10C12.2386 10 10 12.2386 10 15C10 15.8503 10.2123 16.6511 10.5867 17.3522Z" fill="white"/>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(45,9,9)"/>
|
<path d="M10 6.39999C10 6.67614 9.77614 6.89999 9.5 6.89999C9.22386 6.89999 9 6.67614 9 6.39999C9 6.12385 9.22386 5.89999 9.5 5.89999C9.77614 5.89999 10 6.12385 10 6.39999Z" fill="black"/>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(90,9,9)"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4 0 0 4 0 9C0 14 4 18 9 18C9.63967 18 10.263 17.9345 10.8636 17.8099C10.3186 17.0091 10 16.0417 10 15C10 12.2386 12.2386 10 15 10C16.0417 10 17.0091 10.3186 17.8099 10.8636C17.9345 10.263 18 9.63967 18 9C18 4 14 0 9 0ZM1.2 10.8L4.7 8.5V8.2C4.7 6.4 6 5 7.8 4.8H8.2C9.4 4.8 10.5 5.4 11.1 6.4C11.4 6.3 11.7 6.3 12 6.3C12.4 6.3 12.8 6.3 13.2 6.4C13.9 6.6 14.6 6.9 15.2 7.3C14.6 7.1 14 7 13.3 7C12.1 7 11.1 7.4 10.4 8.4C9.7 9.3 9.3 10.4 9.3 11.6C9.3 13.1 8.9 14.5 8 15.8C7.93744 15.8834 7.87923 15.9625 7.82356 16.0381C7.6123 16.325 7.43739 16.5626 7.2 16.8C4.2 16.1 1.9 13.8 1.2 10.8Z" fill="black"/>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(135,9,9)"/>
|
<path d="M18 15C18 16.6569 16.6569 18 15 18C13.3431 18 12 16.6569 12 15C12 13.3431 13.3431 12 15 12C16.6569 12 18 13.3431 18 15Z" fill="#45B83B"/>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(180,9,9)"/>
|
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(225,9,9)"/>
|
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(270,9,9)"/>
|
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(315,9,9)"/>
|
|
||||||
</g>
|
</g>
|
||||||
<circle cx="9" cy="9" r="2.2" fill="#cc2000"/>
|
<defs>
|
||||||
<circle cx="14.5" cy="14.5" r="3" fill="#45B83B"/>
|
<clipPath id="clip0_2707_1961">
|
||||||
|
<rect width="18" height="18" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 841 B After Width: | Height: | Size: 1.5 KiB |
@@ -1,14 +1,13 @@
|
|||||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g fill="#980000" fill-opacity="0.88">
|
<g clip-path="url(#clip0_2707_2015)">
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(0,9,9)"/>
|
<path d="M10.5867 17.3522C10.0727 17.4492 9.54226 17.5 9 17.5C4.30558 17.5 0.5 13.6944 0.5 9C0.5 4.30558 4.30558 0.5 9 0.5C13.6944 0.5 17.5 4.30558 17.5 9C17.5 9.54226 17.4492 10.0727 17.3522 10.5867C16.6511 10.2123 15.8503 10 15 10C12.2386 10 10 12.2386 10 15C10 15.8503 10.2123 16.6511 10.5867 17.3522Z" fill="white"/>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(45,9,9)"/>
|
<path d="M10 6.39999C10 6.67614 9.77614 6.89999 9.5 6.89999C9.22386 6.89999 9 6.67614 9 6.39999C9 6.12385 9.22386 5.89999 9.5 5.89999C9.77614 5.89999 10 6.12385 10 6.39999Z" fill="black"/>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(90,9,9)"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4 0 0 4 0 9C0 14 4 18 9 18C9.63967 18 10.263 17.9345 10.8636 17.8099C10.3186 17.0091 10 16.0417 10 15C10 12.2386 12.2386 10 15 10C16.0417 10 17.0091 10.3186 17.8099 10.8636C17.9345 10.263 18 9.63967 18 9C18 4 14 0 9 0ZM1.2 10.8L4.7 8.5V8.2C4.7 6.4 6 5 7.8 4.8H8.2C9.4 4.8 10.5 5.4 11.1 6.4C11.4 6.3 11.7 6.3 12 6.3C12.4 6.3 12.8 6.3 13.2 6.4C13.9 6.6 14.6 6.9 15.2 7.3C14.6 7.1 14 7 13.3 7C12.1 7 11.1 7.4 10.4 8.4C9.7 9.3 9.3 10.4 9.3 11.6C9.3 13.1 8.9 14.5 8 15.8C7.93744 15.8834 7.87923 15.9625 7.82356 16.0381C7.6123 16.325 7.43739 16.5626 7.2 16.8C4.2 16.1 1.9 13.8 1.2 10.8Z" fill="black"/>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(135,9,9)"/>
|
<path d="M18 15C18 16.6569 16.6569 18 15 18C13.3431 18 12 16.6569 12 15C12 13.3431 13.3431 12 15 12C16.6569 12 18 13.3431 18 15Z" fill="#989898"/>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(180,9,9)"/>
|
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(225,9,9)"/>
|
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(270,9,9)"/>
|
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(315,9,9)"/>
|
|
||||||
</g>
|
</g>
|
||||||
<circle cx="9" cy="9" r="2.2" fill="#cc2000"/>
|
<defs>
|
||||||
<circle cx="14.5" cy="14.5" r="3" fill="#989898"/>
|
<clipPath id="clip0_2707_2015">
|
||||||
|
<rect width="18" height="18" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 841 B After Width: | Height: | Size: 1.5 KiB |
@@ -1,13 +1,19 @@
|
|||||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In -->
|
||||||
<g fill="#980000" fill-opacity="0.88">
|
<svg version="1.1"
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(0,9,9)"/>
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(45,9,9)"/>
|
x="0px" y="0px" width="18px" height="18px" viewBox="0 0 18 18" enable-background="new 0 0 18 18" xml:space="preserve">
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(90,9,9)"/>
|
<defs>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(135,9,9)"/>
|
</defs>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(180,9,9)"/>
|
<g>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(225,9,9)"/>
|
<g>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(270,9,9)"/>
|
<circle fill="#FFFFFF" cx="9" cy="9" r="8.5"/>
|
||||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(315,9,9)"/>
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="M9,0C4,0,0,4,0,9c0,5,4,9,9,9c5,0,9-4,9-9C18,4,14,0,9,0z M1.2,10.8l3.5-2.3c0-0.1,0-0.2,0-0.3c0-1.8,1.3-3.2,3.1-3.4
|
||||||
|
c0.1,0,0.2,0,0.4,0c1.2,0,2.3,0.6,2.9,1.6c0.3-0.1,0.6-0.1,0.9-0.1c0.4,0,0.8,0,1.2,0.1c0.7,0.2,1.4,0.5,2,0.9
|
||||||
|
C14.6,7.1,14,7,13.3,7c-1.2,0-2.2,0.4-2.9,1.4c-0.7,0.9-1.1,2-1.1,3.2c0,1.5-0.4,2.9-1.3,4.2c-0.3,0.4-0.5,0.7-0.8,1
|
||||||
|
C4.2,16.1,1.9,13.8,1.2,10.8z"/>
|
||||||
|
<circle cx="9.5" cy="6.4" r="0.5"/>
|
||||||
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<circle cx="9" cy="9" r="2.2" fill="#cc2000"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 788 B After Width: | Height: | Size: 871 B |
@@ -1,48 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
const version = process.argv[2];
|
|
||||||
|
|
||||||
if (!version) {
|
|
||||||
console.error("Version argument missing");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = path.resolve(__dirname, "..");
|
|
||||||
const newVersionTag = `v${version}`;
|
|
||||||
|
|
||||||
// Update package.json + package-lock.json safely
|
|
||||||
execSync(`npm version ${version} --no-git-tag-version`, {
|
|
||||||
cwd: root,
|
|
||||||
stdio: "inherit",
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Updated package.json and package-lock.json → ${version}`);
|
|
||||||
|
|
||||||
// Update UI version references
|
|
||||||
const files = [
|
|
||||||
"src/app/features/settings/about/About.tsx",
|
|
||||||
"src/app/pages/auth/AuthFooter.tsx",
|
|
||||||
"src/app/pages/client/WelcomePage.tsx",
|
|
||||||
];
|
|
||||||
|
|
||||||
files.forEach((filePath) => {
|
|
||||||
const absPath = path.join(root, filePath);
|
|
||||||
|
|
||||||
if (!fs.existsSync(absPath)) {
|
|
||||||
console.warn(`File not found: ${filePath}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = fs.readFileSync(absPath, "utf8");
|
|
||||||
const updated = content.replace(/v\d+\.\d+\.\d+/g, newVersionTag);
|
|
||||||
|
|
||||||
fs.writeFileSync(absPath, updated);
|
|
||||||
|
|
||||||
console.log(`Updated ${filePath} → ${newVersionTag}`);
|
|
||||||
});
|
|
||||||
@@ -51,12 +51,8 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
|||||||
},
|
},
|
||||||
location.pathname
|
location.pathname
|
||||||
);
|
);
|
||||||
const encodedSpaceIdOrAlias = spaceMatch?.params.spaceIdOrAlias;
|
if (spaceMatch?.params.spaceIdOrAlias) {
|
||||||
const decodedSpaceIdOrAlias =
|
navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias));
|
||||||
encodedSpaceIdOrAlias && decodeURIComponent(encodedSpaceIdOrAlias);
|
|
||||||
|
|
||||||
if (decodedSpaceIdOrAlias) {
|
|
||||||
navigate(getSpacePath(decodedSpaceIdOrAlias));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,497 +0,0 @@
|
|||||||
/* eslint-disable jsx-a11y/media-has-caption */
|
|
||||||
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
|
||||||
import { MatrixRTCSession } from 'matrix-js-sdk/lib/matrixrtc/MatrixRTCSession';
|
|
||||||
import FocusTrap from 'focus-trap-react';
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
color,
|
|
||||||
config,
|
|
||||||
Dialog,
|
|
||||||
Icon,
|
|
||||||
Icons,
|
|
||||||
Overlay,
|
|
||||||
OverlayBackdrop,
|
|
||||||
OverlayCenter,
|
|
||||||
Text,
|
|
||||||
toRem,
|
|
||||||
} from 'folds';
|
|
||||||
import {
|
|
||||||
EventTimelineSetHandlerMap,
|
|
||||||
EventType,
|
|
||||||
RelationType,
|
|
||||||
Room,
|
|
||||||
RoomEvent,
|
|
||||||
} from 'matrix-js-sdk';
|
|
||||||
import { IRTCNotificationContent, RTCNotificationType } from 'matrix-js-sdk/lib/matrixrtc/types';
|
|
||||||
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
|
|
||||||
import {
|
|
||||||
CallEmbedContextProvider,
|
|
||||||
CallEmbedRefContextProvider,
|
|
||||||
useCallHangupEvent,
|
|
||||||
useCallJoined,
|
|
||||||
useCallThemeSync,
|
|
||||||
useCallMemberSoundSync,
|
|
||||||
useCallStart,
|
|
||||||
} from '../hooks/useCallEmbed';
|
|
||||||
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
|
|
||||||
import { CallEmbed } from '../plugins/call';
|
|
||||||
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
|
||||||
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
|
||||||
import { useMatrixClient } from '../hooks/useMatrixClient';
|
|
||||||
import CallSound from '../../../public/sound/call.ogg';
|
|
||||||
import { useCallMembersChange, useCallSession } from '../hooks/useCall';
|
|
||||||
import { useRoomAvatar, useRoomName } from '../hooks/useRoomMeta';
|
|
||||||
import { mDirectAtom } from '../state/mDirectList';
|
|
||||||
import { useMediaAuthentication } from '../hooks/useMediaAuthentication';
|
|
||||||
import { mxcUrlToHttp } from '../utils/matrix';
|
|
||||||
import { RoomAvatar, RoomIcon } from './room-avatar';
|
|
||||||
import { useRoomNavigate } from '../hooks/useRoomNavigate';
|
|
||||||
import { getStateEvent } from '../utils/room';
|
|
||||||
import { StateEvent } from '../../types/matrix/room';
|
|
||||||
import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels';
|
|
||||||
import { getRoomCreatorsForRoomId } from '../hooks/useRoomCreators';
|
|
||||||
import { getRoomPermissionsAPI } from '../hooks/useRoomPermissions';
|
|
||||||
import { useLivekitSupport } from '../hooks/useLivekitSupport';
|
|
||||||
import { CallAvatarAnimation } from '../styles/Animations.css';
|
|
||||||
import { webRTCSupported } from '../utils/rtc';
|
|
||||||
|
|
||||||
const PIP_MIN_W = 200;
|
|
||||||
const PIP_MIN_H = 112;
|
|
||||||
|
|
||||||
type Corner = 'se' | 'sw' | 'ne' | 'nw';
|
|
||||||
|
|
||||||
/** Normalise the element to top/left positioning so resize math is uniform. */
|
|
||||||
function normaliseToTopLeft(el: HTMLElement) {
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
el.style.left = `${rect.left}px`;
|
|
||||||
el.style.top = `${rect.top}px`;
|
|
||||||
el.style.right = 'auto';
|
|
||||||
el.style.bottom = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
type IncomingCallInfo = {
|
|
||||||
room: Room;
|
|
||||||
sender: string;
|
|
||||||
senderTs: number;
|
|
||||||
lifetime: number;
|
|
||||||
intent?: string;
|
|
||||||
notificationType: RTCNotificationType;
|
|
||||||
refEventId: string;
|
|
||||||
};
|
|
||||||
type IncomingCallProps = {
|
|
||||||
dm: boolean;
|
|
||||||
info: IncomingCallInfo;
|
|
||||||
onIgnore: () => void;
|
|
||||||
onAnswer: (room: Room, video: boolean) => void;
|
|
||||||
onReject: (room: Room, eventId: string) => void;
|
|
||||||
};
|
|
||||||
function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallProps) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const useAuthentication = useMediaAuthentication();
|
|
||||||
const livekitSupported = useLivekitSupport();
|
|
||||||
const rtcSupported = webRTCSupported();
|
|
||||||
const canAnswer = livekitSupported && rtcSupported;
|
|
||||||
const { room } = info;
|
|
||||||
|
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
|
||||||
|
|
||||||
const roomName = useRoomName(room);
|
|
||||||
const roomAvatar = useRoomAvatar(room, dm);
|
|
||||||
const avatarUrl = roomAvatar
|
|
||||||
? mxcUrlToHttp(mx, roomAvatar, useAuthentication, 96, 96, 'crop') ?? undefined
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const session = useCallSession(room);
|
|
||||||
useCallMembersChange(
|
|
||||||
session,
|
|
||||||
useCallback(() => {
|
|
||||||
const members = MatrixRTCSession.sessionMembershipsForRoom(room, session.sessionDescription);
|
|
||||||
if (members.length === 0) {
|
|
||||||
onIgnore();
|
|
||||||
}
|
|
||||||
}, [room, session, onIgnore])
|
|
||||||
);
|
|
||||||
|
|
||||||
const playSound = useCallback(() => {
|
|
||||||
const audioElement = audioRef.current;
|
|
||||||
audioElement?.play();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (info.notificationType === 'ring') {
|
|
||||||
playSound();
|
|
||||||
}
|
|
||||||
}, [playSound, info.notificationType]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
|
||||||
<OverlayCenter>
|
|
||||||
<FocusTrap
|
|
||||||
focusTrapOptions={{
|
|
||||||
initialFocus: false,
|
|
||||||
onDeactivate: () => onIgnore(),
|
|
||||||
clickOutsideDeactivates: false,
|
|
||||||
escapeDeactivates: false,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Dialog style={{ maxWidth: toRem(324) }}>
|
|
||||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="700">
|
|
||||||
<Text size="T200" align="Center">
|
|
||||||
{info.sender}
|
|
||||||
</Text>
|
|
||||||
<Box direction="Column" gap="500" alignItems="Center">
|
|
||||||
<Box shrink="No">
|
|
||||||
<Avatar size="500" className={CallAvatarAnimation}>
|
|
||||||
<RoomAvatar
|
|
||||||
roomId={room.roomId}
|
|
||||||
src={avatarUrl}
|
|
||||||
alt={roomName}
|
|
||||||
renderFallback={() => (
|
|
||||||
<RoomIcon
|
|
||||||
roomType={room.getType()}
|
|
||||||
size="400"
|
|
||||||
joinRule={room.getJoinRule()}
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Avatar>
|
|
||||||
</Box>
|
|
||||||
<Box grow="Yes" direction="Column" gap="100">
|
|
||||||
<Text size="H3" align="Center" truncate>
|
|
||||||
{roomName}
|
|
||||||
</Text>
|
|
||||||
<Text size="T300">Incoming Call</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{!livekitSupported && (
|
|
||||||
<Text
|
|
||||||
style={{ margin: 'auto', color: color.Critical.Main }}
|
|
||||||
size="L400"
|
|
||||||
align="Center"
|
|
||||||
>
|
|
||||||
Your homeserver does not support calling.
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{!webRTCSupported && (
|
|
||||||
<Text
|
|
||||||
style={{ margin: 'auto', color: color.Critical.Main }}
|
|
||||||
size="L400"
|
|
||||||
align="Center"
|
|
||||||
>
|
|
||||||
Your browser does not support WebRTC, which is required for calling.
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<Box direction="Column" gap="300">
|
|
||||||
<Button
|
|
||||||
style={{ flexGrow: 1 }}
|
|
||||||
variant="Success"
|
|
||||||
size="400"
|
|
||||||
radii="400"
|
|
||||||
onClick={() => onAnswer(room, info.intent === 'video')}
|
|
||||||
before={
|
|
||||||
<Icon
|
|
||||||
size="200"
|
|
||||||
src={info.intent === 'video' ? Icons.VideoCamera : Icons.Phone}
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
disabled={!canAnswer}
|
|
||||||
>
|
|
||||||
<Text as="span" size="B400">
|
|
||||||
Answer
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
style={{ flexGrow: 1 }}
|
|
||||||
variant="Success"
|
|
||||||
fill="Soft"
|
|
||||||
size="400"
|
|
||||||
radii="400"
|
|
||||||
onClick={() => (dm ? onReject(room, info.refEventId) : onIgnore())}
|
|
||||||
before={<Icon size="200" src={Icons.Cross} filled />}
|
|
||||||
>
|
|
||||||
<Text as="span" size="B400">
|
|
||||||
{dm ? 'Reject' : 'Ignore'}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Dialog>
|
|
||||||
</FocusTrap>
|
|
||||||
</OverlayCenter>
|
|
||||||
</Overlay>
|
|
||||||
<audio ref={audioRef} loop style={{ display: 'none' }}>
|
|
||||||
<source src={CallSound} type="audio/ogg" />
|
|
||||||
</audio>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type IncomingCallListenerProps = {
|
|
||||||
callEmbed?: CallEmbed;
|
|
||||||
joined?: boolean;
|
|
||||||
};
|
|
||||||
function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const directs = useAtomValue(mDirectAtom);
|
|
||||||
const { navigateRoom } = useRoomNavigate();
|
|
||||||
|
|
||||||
const [callInfo, setCallInfo] = useState<IncomingCallInfo>();
|
|
||||||
const dm = callInfo ? directs.has(callInfo.room.roomId) : false;
|
|
||||||
const startCall = useCallStart(dm);
|
|
||||||
|
|
||||||
const handleTimelineEvent: EventTimelineSetHandlerMap[RoomEvent.Timeline] = useCallback(
|
|
||||||
async (event, room, toStartOfTimeline, removed, data) => {
|
|
||||||
// only process rtc notification reference events.
|
|
||||||
// we do not want to wait to decrypt all events.
|
|
||||||
if (event.getRelation()?.rel_type !== RelationType.Reference) return;
|
|
||||||
|
|
||||||
if (event.isEncrypted()) {
|
|
||||||
if (!event.isBeingDecrypted()) {
|
|
||||||
await event.attemptDecryption(mx.getCrypto() as CryptoBackend);
|
|
||||||
}
|
|
||||||
await event.getDecryptionPromise();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!room ||
|
|
||||||
event.getType() !== EventType.RTCNotification ||
|
|
||||||
event.getSender() === mx.getSafeUserId() ||
|
|
||||||
!data.liveEvent
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sender = event.getSender();
|
|
||||||
const content = event.getContent<IRTCNotificationContent>();
|
|
||||||
const senderTs =
|
|
||||||
content.sender_ts - event.getTs() > 20000 ? event.getTs() : content.sender_ts;
|
|
||||||
const lifetime = Math.min(content.lifetime, 120000);
|
|
||||||
const notificationType = content.notification_type;
|
|
||||||
const relation =
|
|
||||||
event.getRelation()?.rel_type === RelationType.Reference ? event.getRelation() : undefined;
|
|
||||||
const refEventId = relation?.event_id;
|
|
||||||
|
|
||||||
const mention =
|
|
||||||
content['m.mentions'].room || content['m.mentions'].user_ids?.includes(mx.getSafeUserId());
|
|
||||||
if (!sender || !refEventId || !mention || Date.now() >= senderTs + lifetime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const powerLevelsEvent = getStateEvent(room, StateEvent.RoomPowerLevels);
|
|
||||||
const powerLevels = getPowersLevelFromMatrixEvent(powerLevelsEvent);
|
|
||||||
const creators = getRoomCreatorsForRoomId(mx, room.roomId);
|
|
||||||
const permissions = getRoomPermissionsAPI(creators, powerLevels);
|
|
||||||
|
|
||||||
const hasCallPermission = permissions.stateEvent(
|
|
||||||
StateEvent.GroupCallMemberPrefix,
|
|
||||||
mx.getSafeUserId()
|
|
||||||
);
|
|
||||||
if (!hasCallPermission) return;
|
|
||||||
|
|
||||||
const info: IncomingCallInfo = {
|
|
||||||
room,
|
|
||||||
sender,
|
|
||||||
senderTs,
|
|
||||||
lifetime,
|
|
||||||
intent:
|
|
||||||
'm.call.intent' in content && typeof content['m.call.intent'] === 'string'
|
|
||||||
? content['m.call.intent']
|
|
||||||
: undefined,
|
|
||||||
notificationType,
|
|
||||||
refEventId,
|
|
||||||
};
|
|
||||||
|
|
||||||
setCallInfo(info);
|
|
||||||
},
|
|
||||||
[mx]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
mx.on(RoomEvent.Timeline, handleTimelineEvent);
|
|
||||||
return () => {
|
|
||||||
mx.removeListener(RoomEvent.Timeline, handleTimelineEvent);
|
|
||||||
};
|
|
||||||
}, [mx, handleTimelineEvent]);
|
|
||||||
|
|
||||||
const handleIgnore = useCallback(() => {
|
|
||||||
setCallInfo(undefined);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleReject = useCallback(
|
|
||||||
(room: Room, eventId: string) => {
|
|
||||||
mx.sendEvent(room.roomId, EventType.RTCDecline, {
|
|
||||||
'm.relates_to': {
|
|
||||||
rel_type: RelationType.Reference,
|
|
||||||
event_id: eventId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setCallInfo(undefined);
|
|
||||||
},
|
|
||||||
[mx]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAnswer = useCallback(
|
|
||||||
(room: Room, video: boolean) => {
|
|
||||||
startCall(room, { microphone: true, video, sound: true });
|
|
||||||
setCallInfo(undefined);
|
|
||||||
navigateRoom(room.roomId);
|
|
||||||
},
|
|
||||||
[startCall, navigateRoom]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (callInfo && callEmbed?.roomId === callInfo.room.roomId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return !joined && callInfo ? (
|
|
||||||
<IncomingCall
|
|
||||||
dm={dm}
|
|
||||||
info={callInfo}
|
|
||||||
onIgnore={handleIgnore}
|
|
||||||
onAnswer={handleAnswer}
|
|
||||||
onReject={handleReject}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CallUtils({ embed }: { embed: CallEmbed }) {
|
|
||||||
const setCallEmbed = useSetAtom(callEmbedAtom);
|
|
||||||
|
|
||||||
useCallMemberSoundSync(embed);
|
|
||||||
useCallThemeSync(embed);
|
|
||||||
useCallHangupEvent(
|
|
||||||
embed,
|
|
||||||
useCallback(() => {
|
|
||||||
setCallEmbed(undefined);
|
|
||||||
}, [setCallEmbed])
|
|
||||||
);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
type CallEmbedProviderProps = {
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|
||||||
const callEmbed = useAtomValue(callEmbedAtom);
|
|
||||||
const callEmbedRef = useRef<HTMLDivElement>(null);
|
|
||||||
const joined = useCallJoined(callEmbed);
|
|
||||||
|
|
||||||
const selectedRoom = useSelectedRoom();
|
|
||||||
const chat = useAtomValue(callChatAtom);
|
|
||||||
const screenSize = useScreenSizeContext();
|
|
||||||
|
|
||||||
const chatOnlyView = chat && screenSize !== ScreenSize.Desktop;
|
|
||||||
const inCallRoom = callEmbed && selectedRoom === callEmbed.roomId;
|
|
||||||
const callActive = callEmbed && joined;
|
|
||||||
const callVisible = inCallRoom && callActive && !chatOnlyView;
|
|
||||||
const pipMode = callActive && !inCallRoom;
|
|
||||||
const { navigateRoom } = useRoomNavigate();
|
|
||||||
|
|
||||||
const pipDragRef = React.useRef<{
|
|
||||||
startX: number; startY: number; origLeft: number; origTop: number; dragged: boolean;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const el = callEmbedRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
if (pipMode) {
|
|
||||||
el.style.top = 'auto'; el.style.left = 'auto';
|
|
||||||
el.style.bottom = '72px'; el.style.right = '16px';
|
|
||||||
el.style.width = '280px'; el.style.height = '158px';
|
|
||||||
el.style.borderRadius = '12px'; el.style.overflow = 'hidden';
|
|
||||||
el.style.zIndex = '99'; el.style.boxShadow = '0 8px 32px rgba(0,0,0,0.55)';
|
|
||||||
el.style.border = '1px solid rgba(255,255,255,0.1)'; el.style.visibility = 'visible';
|
|
||||||
} else {
|
|
||||||
['top','left','bottom','right','width','height','borderRadius','overflow','zIndex','boxShadow','border'].forEach(p => { (el.style as any)[p] = ''; });
|
|
||||||
el.style.visibility = callVisible ? '' : 'hidden';
|
|
||||||
}
|
|
||||||
}, [pipMode, callVisible]);
|
|
||||||
|
|
||||||
const handlePipMouseDown = (e: React.MouseEvent) => {
|
|
||||||
const el = callEmbedRef.current; if (!el) return;
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
pipDragRef.current = { startX: e.clientX, startY: e.clientY, origLeft: rect.left, origTop: rect.top, dragged: false };
|
|
||||||
const onMove = (ev: MouseEvent) => {
|
|
||||||
if (!pipDragRef.current || !el) return;
|
|
||||||
const dx = ev.clientX - pipDragRef.current.startX, dy = ev.clientY - pipDragRef.current.startY;
|
|
||||||
if (!pipDragRef.current.dragged && Math.abs(dx)+Math.abs(dy) > 5) { pipDragRef.current.dragged = true; document.body.style.cursor = 'grabbing'; document.body.style.userSelect = 'none'; }
|
|
||||||
if (pipDragRef.current.dragged) {
|
|
||||||
el.style.left = `${Math.max(0, Math.min(window.innerWidth-el.offsetWidth, pipDragRef.current.origLeft+dx))}px`;
|
|
||||||
el.style.top = `${Math.max(0, Math.min(window.innerHeight-el.offsetHeight, pipDragRef.current.origTop+dy))}px`;
|
|
||||||
el.style.right = 'auto'; el.style.bottom = 'auto';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; setTimeout(() => { if (pipDragRef.current) pipDragRef.current.dragged = false; }, 0); };
|
|
||||||
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResizeMouseDown = (e: React.MouseEvent, corner: Corner) => {
|
|
||||||
e.stopPropagation(); e.preventDefault();
|
|
||||||
const el = callEmbedRef.current; if (!el) return;
|
|
||||||
normaliseToTopLeft(el);
|
|
||||||
const sx = e.clientX, sy = e.clientY, sw = el.offsetWidth, sh = el.offsetHeight;
|
|
||||||
const sl = parseFloat(el.style.left), st = parseFloat(el.style.top);
|
|
||||||
document.body.style.cursor = `${corner}-resize`; document.body.style.userSelect = 'none';
|
|
||||||
const onMove = (ev: MouseEvent) => {
|
|
||||||
const dx = ev.clientX-sx, dy = ev.clientY-sy;
|
|
||||||
let w = sw, h = sh, l = sl, t = st;
|
|
||||||
if (corner==='se'){w=sw+dx;h=sh+dy;} if (corner==='sw'){w=sw-dx;h=sh+dy;l=sl+sw-Math.max(PIP_MIN_W,w);}
|
|
||||||
if (corner==='ne'){w=sw+dx;h=sh-dy;t=st+sh-Math.max(PIP_MIN_H,h);} if (corner==='nw'){w=sw-dx;h=sh-dy;l=sl+sw-Math.max(PIP_MIN_W,w);t=st+sh-Math.max(PIP_MIN_H,h);}
|
|
||||||
w=Math.max(PIP_MIN_W,Math.min(w,window.innerWidth)); h=Math.max(PIP_MIN_H,Math.min(h,window.innerHeight));
|
|
||||||
l=Math.max(0,Math.min(l,window.innerWidth-PIP_MIN_W)); t=Math.max(0,Math.min(t,window.innerHeight-PIP_MIN_H));
|
|
||||||
el.style.width=`${w}px`; el.style.height=`${h}px`; el.style.left=`${l}px`; el.style.top=`${t}px`;
|
|
||||||
};
|
|
||||||
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor=''; document.body.style.userSelect=''; };
|
|
||||||
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CallEmbedContextProvider value={callEmbed}>
|
|
||||||
{callEmbed && <CallUtils embed={callEmbed} />}
|
|
||||||
<CallEmbedRefContextProvider value={callEmbedRef}>
|
|
||||||
<IncomingCallListener callEmbed={callEmbed} joined={joined} />
|
|
||||||
{children}
|
|
||||||
</CallEmbedRefContextProvider>
|
|
||||||
<div
|
|
||||||
data-call-embed-container
|
|
||||||
style={{
|
|
||||||
visibility: callVisible ? undefined : 'hidden',
|
|
||||||
position: 'fixed',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: '100%',
|
|
||||||
height: '50%',
|
|
||||||
}}
|
|
||||||
ref={callEmbedRef}
|
|
||||||
>
|
|
||||||
{pipMode && callEmbed && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label="Return to call"
|
|
||||||
onMouseDown={handlePipMouseDown}
|
|
||||||
onClick={() => { if (!pipDragRef.current?.dragged) navigateRoom(callEmbed.roomId); }}
|
|
||||||
onKeyDown={(e) => e.key === 'Enter' && navigateRoom(callEmbed.roomId)}
|
|
||||||
style={{ position:'absolute', inset:0, zIndex:1, background:'transparent', cursor:'grab', display:'flex', alignItems:'flex-start', justifyContent:'flex-end', padding:'6px' }}
|
|
||||||
>
|
|
||||||
<div style={{ background:'rgba(0,0,0,0.65)', backdropFilter:'blur(4px)', borderRadius:'6px', padding:'3px 8px', color:'#fff', fontSize:'11px', fontWeight:600, pointerEvents:'none' }}>
|
|
||||||
↗ Return to call
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{(['se','sw','ne','nw'] as Corner[]).map((corner) => (
|
|
||||||
<div key={corner} onMouseDown={(e) => handleResizeMouseDown(e, corner)} onClick={(e) => e.stopPropagation()}
|
|
||||||
style={{ position:'absolute', width:'18px', height:'18px', [corner.includes('s')?'bottom':'top']:0, [corner.includes('e')?'right':'left']:0, cursor:`${corner}-resize`, zIndex:2 }} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CallEmbedContextProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import FocusTrap from 'focus-trap-react';
|
|
||||||
import { Grid, SearchBar, SearchContext, SearchContextManager } from '@giphy/react-components';
|
|
||||||
import { IGif } from '@giphy/js-types';
|
|
||||||
import { Box } from 'folds';
|
|
||||||
import { useSetting } from '../state/hooks/settings';
|
|
||||||
import { settingsAtom } from '../state/settings';
|
|
||||||
|
|
||||||
const PICKER_WIDTH = 312;
|
|
||||||
|
|
||||||
const TERMINAL_CSS = `
|
|
||||||
[data-gif-terminal] input,
|
|
||||||
[data-gif-terminal] form {
|
|
||||||
background: #030c14 !important;
|
|
||||||
color: #e8edf5 !important;
|
|
||||||
font-family: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace !important;
|
|
||||||
border: 1px solid rgba(255,107,0,0.35) !important;
|
|
||||||
border-radius: 4px !important;
|
|
||||||
font-size: 12px !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
[data-gif-terminal] input:focus {
|
|
||||||
border-color: rgba(255,107,0,0.7) !important;
|
|
||||||
box-shadow: 0 0 0 2px rgba(255,107,0,0.12) !important;
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
[data-gif-terminal] input::placeholder {
|
|
||||||
color: rgba(255,107,0,0.4) !important;
|
|
||||||
font-family: 'JetBrains Mono', monospace !important;
|
|
||||||
}
|
|
||||||
[data-gif-terminal] svg,
|
|
||||||
[data-gif-terminal] button[type="reset"] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
[data-gif-terminal] ::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
[data-gif-terminal] ::-webkit-scrollbar-track {
|
|
||||||
background: #030508;
|
|
||||||
}
|
|
||||||
[data-gif-terminal] ::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255,107,0,0.4);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
type GifPickerInnerProps = {
|
|
||||||
onSelect: (url: string, width: number, height: number) => void;
|
|
||||||
requestClose: () => void;
|
|
||||||
lotusTerminal: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInnerProps) {
|
|
||||||
const { fetchGifs, searchKey } = React.useContext(SearchContext);
|
|
||||||
|
|
||||||
const handleClick = useCallback(
|
|
||||||
(gif: IGif, e: React.SyntheticEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const r = gif.images.downsized ?? gif.images.original;
|
|
||||||
const url = r.url;
|
|
||||||
const width = Number(r.width) || 200;
|
|
||||||
const height = Number(r.height) || 200;
|
|
||||||
onSelect(url, width, height);
|
|
||||||
requestClose();
|
|
||||||
},
|
|
||||||
[onSelect, requestClose]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box direction="Column" style={{ width: `${PICKER_WIDTH}px` }}>
|
|
||||||
{lotusTerminal && (
|
|
||||||
<div style={{
|
|
||||||
padding: '5px 10px 4px',
|
|
||||||
borderBottom: '1px solid rgba(255,107,0,0.2)',
|
|
||||||
fontFamily: "'JetBrains Mono', 'Cascadia Code', monospace",
|
|
||||||
fontSize: '10px',
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: '0.1em',
|
|
||||||
color: '#FF6B00',
|
|
||||||
userSelect: 'none',
|
|
||||||
}}>
|
|
||||||
// GIF_SEARCH
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Box style={{ padding: '8px 8px 4px' }}>
|
|
||||||
<SearchBar style={{ width: '100%', borderRadius: lotusTerminal ? '4px' : '8px' }} />
|
|
||||||
</Box>
|
|
||||||
<div style={{ overflowY: 'auto', overflowX: 'hidden', maxHeight: '340px', padding: '0 8px 8px' }}>
|
|
||||||
<Grid
|
|
||||||
key={searchKey}
|
|
||||||
fetchGifs={fetchGifs}
|
|
||||||
width={PICKER_WIDTH - 16}
|
|
||||||
columns={2}
|
|
||||||
gutter={4}
|
|
||||||
onGifClick={handleClick}
|
|
||||||
hideAttribution={false}
|
|
||||||
noLink
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type GifPickerProps = {
|
|
||||||
apiKey: string;
|
|
||||||
onSelect: (url: string, width: number, height: number) => void;
|
|
||||||
requestClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function GifPicker({ apiKey, onSelect, requestClose }: GifPickerProps) {
|
|
||||||
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
|
||||||
|
|
||||||
const containerStyle = lotusTerminal
|
|
||||||
? {
|
|
||||||
background: '#060c14',
|
|
||||||
border: '1px solid rgba(255,107,0,0.35)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
boxShadow: '0 4px 24px rgba(255,107,0,0.10), 0 0 0 1px rgba(255,107,0,0.08)',
|
|
||||||
width: `${PICKER_WIDTH}px`,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
background: 'var(--bg-surface)',
|
|
||||||
border: '1px solid rgba(255,255,255,0.08)',
|
|
||||||
borderRadius: '12px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
|
|
||||||
width: `${PICKER_WIDTH}px`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FocusTrap
|
|
||||||
focusTrapOptions={{
|
|
||||||
initialFocus: false,
|
|
||||||
onDeactivate: requestClose,
|
|
||||||
clickOutsideDeactivates: true,
|
|
||||||
allowOutsideClick: true,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
direction="Column"
|
|
||||||
data-gif-terminal={lotusTerminal ? '' : undefined}
|
|
||||||
style={containerStyle}
|
|
||||||
>
|
|
||||||
{lotusTerminal && <style>{TERMINAL_CSS}</style>}
|
|
||||||
<SearchContextManager apiKey={apiKey} initialTerm="">
|
|
||||||
<GifPickerInner onSelect={onSelect} requestClose={requestClose} lotusTerminal={!!lotusTerminal} />
|
|
||||||
</SearchContextManager>
|
|
||||||
</Box>
|
|
||||||
</FocusTrap>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import FocusTrap from 'focus-trap-react';
|
|
||||||
import { as, Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
|
|
||||||
import React, { ReactNode } from 'react';
|
|
||||||
import { ModalWide } from '../styles/Modal.css';
|
|
||||||
import { stopPropagation } from '../utils/keyboard';
|
|
||||||
|
|
||||||
export type RenderViewerProps = {
|
|
||||||
src: string;
|
|
||||||
alt: string;
|
|
||||||
requestClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ImageOverlayProps = RenderViewerProps & {
|
|
||||||
viewer: boolean;
|
|
||||||
renderViewer: (props: RenderViewerProps) => ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ImageOverlay = as<'div', ImageOverlayProps>(
|
|
||||||
({ src, alt, viewer, requestClose, renderViewer, ...props }, ref) => (
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
|
||||||
import { Avatar, Box, Button, Text } from 'folds';
|
|
||||||
import { UserAvatar } from './user-avatar';
|
|
||||||
import { useMatrixClient } from '../hooks/useMatrixClient';
|
|
||||||
import { useCallStart } from '../hooks/useCallEmbed';
|
|
||||||
import { useSetting } from '../state/hooks/settings';
|
|
||||||
import { settingsAtom } from '../state/settings';
|
|
||||||
import { useIncomingDmCall } from '../hooks/useIncomingDmCall';
|
|
||||||
import { mxcUrlToHttp } from '../utils/matrix';
|
|
||||||
|
|
||||||
function useRingTone(active: boolean) {
|
|
||||||
const stopRef = useRef<(() => void) | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!active) {
|
|
||||||
stopRef.current?.();
|
|
||||||
stopRef.current = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ctx: AudioContext;
|
|
||||||
try {
|
|
||||||
ctx = new AudioContext();
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
const pulse = (t: number, freq: number, dur: number) => {
|
|
||||||
const osc = ctx.createOscillator();
|
|
||||||
const gain = ctx.createGain();
|
|
||||||
osc.connect(gain);
|
|
||||||
gain.connect(ctx.destination);
|
|
||||||
osc.frequency.value = freq;
|
|
||||||
osc.type = 'sine';
|
|
||||||
gain.gain.setValueAtTime(0, t);
|
|
||||||
gain.gain.linearRampToValueAtTime(0.22, t + 0.04);
|
|
||||||
gain.gain.setValueAtTime(0.22, t + dur - 0.04);
|
|
||||||
gain.gain.linearRampToValueAtTime(0, t + dur);
|
|
||||||
osc.start(t);
|
|
||||||
osc.stop(t + dur);
|
|
||||||
};
|
|
||||||
|
|
||||||
let ringTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
const ring = () => {
|
|
||||||
if (cancelled) return;
|
|
||||||
const now = ctx.currentTime;
|
|
||||||
pulse(now, 880, 0.18);
|
|
||||||
pulse(now + 0.28, 880, 0.18);
|
|
||||||
ringTimer = setTimeout(ring, 2200);
|
|
||||||
};
|
|
||||||
|
|
||||||
ring();
|
|
||||||
|
|
||||||
const stop = () => {
|
|
||||||
cancelled = true;
|
|
||||||
if (ringTimer !== null) clearTimeout(ringTimer);
|
|
||||||
ctx.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
stopRef.current = stop;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stop();
|
|
||||||
stopRef.current = null;
|
|
||||||
};
|
|
||||||
}, [active]);
|
|
||||||
|
|
||||||
return stopRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
function IncomingCallCard() {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const startCall = useCallStart(true);
|
|
||||||
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
|
||||||
const [incoming, dismiss] = useIncomingDmCall();
|
|
||||||
const stopRingRef = useRingTone(!!incoming);
|
|
||||||
|
|
||||||
const stopAndDismiss = useCallback(() => {
|
|
||||||
stopRingRef.current?.();
|
|
||||||
dismiss();
|
|
||||||
}, [dismiss, stopRingRef]);
|
|
||||||
|
|
||||||
const handleAnswer = useCallback(() => {
|
|
||||||
if (!incoming) return;
|
|
||||||
stopRingRef.current?.();
|
|
||||||
startCall(incoming.room);
|
|
||||||
dismiss();
|
|
||||||
}, [incoming, startCall, dismiss, stopRingRef]);
|
|
||||||
|
|
||||||
if (!incoming) return null;
|
|
||||||
|
|
||||||
const callerUser = mx.getUser(incoming.callerId);
|
|
||||||
const callerName = callerUser?.displayName ?? incoming.callerId;
|
|
||||||
const avatarMxc = callerUser?.avatarUrl;
|
|
||||||
const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, undefined, 64, 64, 'crop') ?? undefined : undefined;
|
|
||||||
|
|
||||||
if (lotusTerminal) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
bottom: '80px',
|
|
||||||
right: '24px',
|
|
||||||
zIndex: 9999,
|
|
||||||
background: '#060c14',
|
|
||||||
border: '1px solid rgba(255,107,0,0.45)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxShadow: '0 4px 32px rgba(255,107,0,0.18), 0 0 0 1px rgba(255,107,0,0.08)',
|
|
||||||
padding: '16px',
|
|
||||||
width: '272px',
|
|
||||||
fontFamily: "'JetBrains Mono', monospace",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '10px',
|
|
||||||
letterSpacing: '0.1em',
|
|
||||||
color: '#00D4FF',
|
|
||||||
marginBottom: '10px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '6px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: 'inline-block',
|
|
||||||
width: '7px',
|
|
||||||
height: '7px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
background: '#00FF88',
|
|
||||||
boxShadow: '0 0 6px #00FF88',
|
|
||||||
animation: 'lotus-ring-pulse 1s ease-in-out infinite',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
INCOMING CALL
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '14px' }}>
|
|
||||||
<Avatar size="300" style={{ border: '1px solid rgba(0,212,255,0.3)', borderRadius: '2px', overflow: 'hidden' }}>
|
|
||||||
<UserAvatar
|
|
||||||
userId={incoming.callerId}
|
|
||||||
src={avatarUrl}
|
|
||||||
alt={callerName}
|
|
||||||
renderFallback={() => (
|
|
||||||
<span style={{ fontSize: '16px', color: '#00D4FF', fontFamily: 'JetBrains Mono, monospace' }}>
|
|
||||||
{callerName[0]?.toUpperCase() ?? '?'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: '13px', color: '#e8edf5', fontWeight: 600 }}>{callerName}</div>
|
|
||||||
<div style={{ fontSize: '10px', color: 'rgba(232,237,245,0.45)', marginTop: '2px' }}>
|
|
||||||
{incoming.callerId}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
|
||||||
<button
|
|
||||||
onClick={handleAnswer}
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
background: 'transparent',
|
|
||||||
border: '1px solid #00FF88',
|
|
||||||
borderRadius: '3px',
|
|
||||||
color: '#00FF88',
|
|
||||||
fontFamily: 'JetBrains Mono, monospace',
|
|
||||||
fontSize: '11px',
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: '0.08em',
|
|
||||||
padding: '9px 4px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
boxShadow: '0 0 10px rgba(0,255,136,0.2)',
|
|
||||||
transition: 'box-shadow 0.15s, background 0.15s',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(0,255,136,0.08)';
|
|
||||||
(e.currentTarget as HTMLButtonElement).style.boxShadow = '0 0 16px rgba(0,255,136,0.35)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
(e.currentTarget as HTMLButtonElement).style.background = 'transparent';
|
|
||||||
(e.currentTarget as HTMLButtonElement).style.boxShadow = '0 0 10px rgba(0,255,136,0.2)';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
ANSWER
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={stopAndDismiss}
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
background: 'transparent',
|
|
||||||
border: '1px solid rgba(255,107,0,0.5)',
|
|
||||||
borderRadius: '3px',
|
|
||||||
color: '#FF6B00',
|
|
||||||
fontFamily: 'JetBrains Mono, monospace',
|
|
||||||
fontSize: '11px',
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: '0.08em',
|
|
||||||
padding: '9px 4px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'opacity 0.15s, background 0.15s',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,107,0,0.08)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
(e.currentTarget as HTMLButtonElement).style.background = 'transparent';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
DECLINE
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<style>{`
|
|
||||||
@keyframes lotus-ring-pulse {
|
|
||||||
0%, 100% { opacity: 1; transform: scale(1); }
|
|
||||||
50% { opacity: 0.4; transform: scale(0.7); }
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
bottom: '80px',
|
|
||||||
right: '24px',
|
|
||||||
zIndex: 9999,
|
|
||||||
background: 'var(--bg-surface)',
|
|
||||||
border: '1px solid var(--bg-surface-border)',
|
|
||||||
borderRadius: '16px',
|
|
||||||
boxShadow: '0 8px 40px rgba(0,0,0,0.45)',
|
|
||||||
padding: '16px',
|
|
||||||
width: '272px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '11px',
|
|
||||||
color: 'var(--text-secondary)',
|
|
||||||
marginBottom: '10px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '6px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: 'inline-block',
|
|
||||||
width: '7px',
|
|
||||||
height: '7px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
background: 'var(--clr-success-main)',
|
|
||||||
animation: 'lotus-ring-pulse 1s ease-in-out infinite',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
Incoming call
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '14px' }}>
|
|
||||||
<Avatar size="300">
|
|
||||||
<UserAvatar
|
|
||||||
userId={incoming.callerId}
|
|
||||||
src={avatarUrl}
|
|
||||||
alt={callerName}
|
|
||||||
renderFallback={() => (
|
|
||||||
<Text size="H4">{callerName[0]?.toUpperCase() ?? '?'}</Text>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<Text size="T400" style={{ fontWeight: 600 }}>{callerName}</Text>
|
|
||||||
<Text size="T200" style={{ opacity: 0.5 }}>{incoming.callerId}</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Box direction="Row" gap="200">
|
|
||||||
<Button
|
|
||||||
onClick={handleAnswer}
|
|
||||||
variant="Success"
|
|
||||||
fill="Solid"
|
|
||||||
size="400"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
<Text size="B400">Answer</Text>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={stopAndDismiss}
|
|
||||||
variant="Critical"
|
|
||||||
fill="Solid"
|
|
||||||
size="400"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
<Text size="B400">Decline</Text>
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<style>{`
|
|
||||||
@keyframes lotus-ring-pulse {
|
|
||||||
0%, 100% { opacity: 1; transform: scale(1); }
|
|
||||||
50% { opacity: 0.4; transform: scale(0.7); }
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function IncomingCallNotification() {
|
|
||||||
return <IncomingCallCard />;
|
|
||||||
}
|
|
||||||
@@ -16,24 +16,34 @@ import {
|
|||||||
import { JoinRule } from 'matrix-js-sdk';
|
import { JoinRule } from 'matrix-js-sdk';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { stopPropagation } from '../utils/keyboard';
|
import { stopPropagation } from '../utils/keyboard';
|
||||||
import { getRoomIconSrc } from '../utils/room';
|
|
||||||
|
|
||||||
export type ExtraJoinRules = 'knock_restricted';
|
export type ExtraJoinRules = 'knock_restricted';
|
||||||
export type ExtendedJoinRules = JoinRule | ExtraJoinRules;
|
export type ExtendedJoinRules = JoinRule | ExtraJoinRules;
|
||||||
|
|
||||||
type JoinRuleIcons = Record<ExtendedJoinRules, IconSrc>;
|
type JoinRuleIcons = Record<ExtendedJoinRules, IconSrc>;
|
||||||
|
export const useRoomJoinRuleIcon = (): JoinRuleIcons =>
|
||||||
export const useJoinRuleIcons = (roomType?: string): JoinRuleIcons =>
|
|
||||||
useMemo(
|
useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
[JoinRule.Invite]: getRoomIconSrc(Icons, roomType, JoinRule.Invite),
|
[JoinRule.Invite]: Icons.HashLock,
|
||||||
[JoinRule.Knock]: getRoomIconSrc(Icons, roomType, JoinRule.Knock),
|
[JoinRule.Knock]: Icons.HashLock,
|
||||||
knock_restricted: getRoomIconSrc(Icons, roomType, JoinRule.Restricted),
|
knock_restricted: Icons.Hash,
|
||||||
[JoinRule.Restricted]: getRoomIconSrc(Icons, roomType, JoinRule.Restricted),
|
[JoinRule.Restricted]: Icons.Hash,
|
||||||
[JoinRule.Public]: getRoomIconSrc(Icons, roomType, JoinRule.Public),
|
[JoinRule.Public]: Icons.HashGlobe,
|
||||||
[JoinRule.Private]: getRoomIconSrc(Icons, roomType, JoinRule.Private),
|
[JoinRule.Private]: Icons.HashLock,
|
||||||
}),
|
}),
|
||||||
[roomType]
|
[]
|
||||||
|
);
|
||||||
|
export const useSpaceJoinRuleIcon = (): JoinRuleIcons =>
|
||||||
|
useMemo(
|
||||||
|
() => ({
|
||||||
|
[JoinRule.Invite]: Icons.SpaceLock,
|
||||||
|
[JoinRule.Knock]: Icons.SpaceLock,
|
||||||
|
knock_restricted: Icons.Space,
|
||||||
|
[JoinRule.Restricted]: Icons.Space,
|
||||||
|
[JoinRule.Public]: Icons.SpaceGlobe,
|
||||||
|
[JoinRule.Private]: Icons.SpaceLock,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
type JoinRuleLabels = Record<ExtendedJoinRules, string>;
|
type JoinRuleLabels = Record<ExtendedJoinRules, string>;
|
||||||
|
|||||||
@@ -2,39 +2,43 @@ import React from 'react';
|
|||||||
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
|
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
|
||||||
import { SequenceCard } from '../sequence-card';
|
import { SequenceCard } from '../sequence-card';
|
||||||
import { SettingTile } from '../setting-tile';
|
import { SettingTile } from '../setting-tile';
|
||||||
import { CreateRoomAccess } from './types';
|
|
||||||
|
|
||||||
type CreateRoomAccessSelectorProps = {
|
export enum CreateRoomKind {
|
||||||
value?: CreateRoomAccess;
|
Private = 'private',
|
||||||
onSelect: (value: CreateRoomAccess) => void;
|
Restricted = 'restricted',
|
||||||
|
Public = 'public',
|
||||||
|
}
|
||||||
|
type CreateRoomKindSelectorProps = {
|
||||||
|
value?: CreateRoomKind;
|
||||||
|
onSelect: (value: CreateRoomKind) => void;
|
||||||
canRestrict?: boolean;
|
canRestrict?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
getIcon: (access: CreateRoomAccess) => IconSrc;
|
getIcon: (kind: CreateRoomKind) => IconSrc;
|
||||||
};
|
};
|
||||||
export function CreateRoomAccessSelector({
|
export function CreateRoomKindSelector({
|
||||||
value,
|
value,
|
||||||
onSelect,
|
onSelect,
|
||||||
canRestrict,
|
canRestrict,
|
||||||
disabled,
|
disabled,
|
||||||
getIcon,
|
getIcon,
|
||||||
}: CreateRoomAccessSelectorProps) {
|
}: CreateRoomKindSelectorProps) {
|
||||||
return (
|
return (
|
||||||
<Box shrink="No" direction="Column" gap="100">
|
<Box shrink="No" direction="Column" gap="100">
|
||||||
{canRestrict && (
|
{canRestrict && (
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
style={{ padding: config.space.S300 }}
|
style={{ padding: config.space.S300 }}
|
||||||
variant={value === CreateRoomAccess.Restricted ? 'Primary' : 'SurfaceVariant'}
|
variant={value === CreateRoomKind.Restricted ? 'Primary' : 'SurfaceVariant'}
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="100"
|
gap="100"
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
aria-pressed={value === CreateRoomAccess.Restricted}
|
aria-pressed={value === CreateRoomKind.Restricted}
|
||||||
onClick={() => onSelect(CreateRoomAccess.Restricted)}
|
onClick={() => onSelect(CreateRoomKind.Restricted)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
before={<Icon size="400" src={getIcon(CreateRoomAccess.Restricted)} />}
|
before={<Icon size="400" src={getIcon(CreateRoomKind.Restricted)} />}
|
||||||
after={value === CreateRoomAccess.Restricted && <Icon src={Icons.Check} />}
|
after={value === CreateRoomKind.Restricted && <Icon src={Icons.Check} />}
|
||||||
>
|
>
|
||||||
<Text size="H6">Restricted</Text>
|
<Text size="H6">Restricted</Text>
|
||||||
<Text size="T300" priority="300">
|
<Text size="T300" priority="300">
|
||||||
@@ -45,18 +49,18 @@ export function CreateRoomAccessSelector({
|
|||||||
)}
|
)}
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
style={{ padding: config.space.S300 }}
|
style={{ padding: config.space.S300 }}
|
||||||
variant={value === CreateRoomAccess.Private ? 'Primary' : 'SurfaceVariant'}
|
variant={value === CreateRoomKind.Private ? 'Primary' : 'SurfaceVariant'}
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="100"
|
gap="100"
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
aria-pressed={value === CreateRoomAccess.Private}
|
aria-pressed={value === CreateRoomKind.Private}
|
||||||
onClick={() => onSelect(CreateRoomAccess.Private)}
|
onClick={() => onSelect(CreateRoomKind.Private)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
before={<Icon size="400" src={getIcon(CreateRoomAccess.Private)} />}
|
before={<Icon size="400" src={getIcon(CreateRoomKind.Private)} />}
|
||||||
after={value === CreateRoomAccess.Private && <Icon src={Icons.Check} />}
|
after={value === CreateRoomKind.Private && <Icon src={Icons.Check} />}
|
||||||
>
|
>
|
||||||
<Text size="H6">Private</Text>
|
<Text size="H6">Private</Text>
|
||||||
<Text size="T300" priority="300">
|
<Text size="T300" priority="300">
|
||||||
@@ -66,18 +70,18 @@ export function CreateRoomAccessSelector({
|
|||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
style={{ padding: config.space.S300 }}
|
style={{ padding: config.space.S300 }}
|
||||||
variant={value === CreateRoomAccess.Public ? 'Primary' : 'SurfaceVariant'}
|
variant={value === CreateRoomKind.Public ? 'Primary' : 'SurfaceVariant'}
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="100"
|
gap="100"
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
aria-pressed={value === CreateRoomAccess.Public}
|
aria-pressed={value === CreateRoomKind.Public}
|
||||||
onClick={() => onSelect(CreateRoomAccess.Public)}
|
onClick={() => onSelect(CreateRoomKind.Public)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
before={<Icon size="400" src={getIcon(CreateRoomAccess.Public)} />}
|
before={<Icon size="400" src={getIcon(CreateRoomKind.Public)} />}
|
||||||
after={value === CreateRoomAccess.Public && <Icon src={Icons.Check} />}
|
after={value === CreateRoomKind.Public && <Icon src={Icons.Check} />}
|
||||||
>
|
>
|
||||||
<Text size="H6">Public</Text>
|
<Text size="H6">Public</Text>
|
||||||
<Text size="T300" priority="300">
|
<Text size="T300" priority="300">
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
|
|
||||||
import { SequenceCard } from '../sequence-card';
|
|
||||||
import { SettingTile } from '../setting-tile';
|
|
||||||
import { CreateRoomType } from './types';
|
|
||||||
import { BetaNoticeBadge } from '../BetaNoticeBadge';
|
|
||||||
|
|
||||||
type CreateRoomTypeSelectorProps = {
|
|
||||||
value?: CreateRoomType;
|
|
||||||
onSelect: (value: CreateRoomType) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
getIcon: (type: CreateRoomType) => IconSrc;
|
|
||||||
};
|
|
||||||
export function CreateRoomTypeSelector({
|
|
||||||
value,
|
|
||||||
onSelect,
|
|
||||||
disabled,
|
|
||||||
getIcon,
|
|
||||||
}: CreateRoomTypeSelectorProps) {
|
|
||||||
return (
|
|
||||||
<Box shrink="No" direction="Column" gap="100">
|
|
||||||
<SequenceCard
|
|
||||||
style={{ padding: config.space.S300 }}
|
|
||||||
variant={value === CreateRoomType.TextRoom ? 'Primary' : 'SurfaceVariant'}
|
|
||||||
direction="Column"
|
|
||||||
gap="100"
|
|
||||||
as="button"
|
|
||||||
type="button"
|
|
||||||
aria-pressed={value === CreateRoomType.TextRoom}
|
|
||||||
onClick={() => onSelect(CreateRoomType.TextRoom)}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<SettingTile
|
|
||||||
before={<Icon size="400" src={getIcon(CreateRoomType.TextRoom)} />}
|
|
||||||
after={value === CreateRoomType.TextRoom && <Icon src={Icons.Check} />}
|
|
||||||
>
|
|
||||||
<Box gap="200" alignItems="Baseline">
|
|
||||||
<Text size="H6" style={{ flexShrink: 0 }}>
|
|
||||||
Chat Room
|
|
||||||
</Text>
|
|
||||||
<Text size="T300" priority="300" truncate>
|
|
||||||
- Messages, photos, and videos.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</SettingTile>
|
|
||||||
</SequenceCard>
|
|
||||||
<SequenceCard
|
|
||||||
style={{ padding: config.space.S300 }}
|
|
||||||
variant={value === CreateRoomType.VoiceRoom ? 'Primary' : 'SurfaceVariant'}
|
|
||||||
direction="Column"
|
|
||||||
gap="100"
|
|
||||||
as="button"
|
|
||||||
type="button"
|
|
||||||
aria-pressed={value === CreateRoomType.VoiceRoom}
|
|
||||||
onClick={() => onSelect(CreateRoomType.VoiceRoom)}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<SettingTile
|
|
||||||
before={<Icon size="400" src={getIcon(CreateRoomType.VoiceRoom)} />}
|
|
||||||
after={value === CreateRoomType.VoiceRoom && <Icon src={Icons.Check} />}
|
|
||||||
>
|
|
||||||
<Box gap="200" alignItems="Baseline">
|
|
||||||
<Text size="H6" style={{ flexShrink: 0 }}>
|
|
||||||
Voice Room
|
|
||||||
</Text>
|
|
||||||
<Text size="T300" priority="300" truncate>
|
|
||||||
- Live audio and video conversations.
|
|
||||||
</Text>
|
|
||||||
<BetaNoticeBadge />
|
|
||||||
</Box>
|
|
||||||
</SettingTile>
|
|
||||||
</SequenceCard>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
export * from './CreateRoomAccessSelector';
|
export * from './CreateRoomKindSelector';
|
||||||
export * from './CreateRoomAliasInput';
|
export * from './CreateRoomAliasInput';
|
||||||
export * from './RoomVersionSelector';
|
export * from './RoomVersionSelector';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './AdditionalCreatorInput';
|
export * from './AdditionalCreatorInput';
|
||||||
export * from './types';
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
export enum CreateRoomType {
|
|
||||||
TextRoom = 'text',
|
|
||||||
VoiceRoom = 'voice',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CreateRoomAccess {
|
|
||||||
Private = 'private',
|
|
||||||
Restricted = 'restricted',
|
|
||||||
Public = 'public',
|
|
||||||
}
|
|
||||||
@@ -7,10 +7,10 @@ import {
|
|||||||
Room,
|
Room,
|
||||||
} from 'matrix-js-sdk';
|
} from 'matrix-js-sdk';
|
||||||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
|
import { CreateRoomKind } from './CreateRoomKindSelector';
|
||||||
import { RoomType, StateEvent } from '../../../types/matrix/room';
|
import { RoomType, StateEvent } from '../../../types/matrix/room';
|
||||||
import { getViaServers } from '../../plugins/via-servers';
|
import { getViaServers } from '../../plugins/via-servers';
|
||||||
import { getMxIdServer } from '../../utils/matrix';
|
import { getMxIdServer } from '../../utils/matrix';
|
||||||
import { CreateRoomAccess } from './types';
|
|
||||||
|
|
||||||
export const createRoomCreationContent = (
|
export const createRoomCreationContent = (
|
||||||
type: RoomType | undefined,
|
type: RoomType | undefined,
|
||||||
@@ -32,7 +32,7 @@ export const createRoomCreationContent = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const createRoomJoinRulesState = (
|
export const createRoomJoinRulesState = (
|
||||||
access: CreateRoomAccess,
|
kind: CreateRoomKind,
|
||||||
parent: Room | undefined,
|
parent: Room | undefined,
|
||||||
knock: boolean
|
knock: boolean
|
||||||
) => {
|
) => {
|
||||||
@@ -40,13 +40,13 @@ export const createRoomJoinRulesState = (
|
|||||||
join_rule: knock ? JoinRule.Knock : JoinRule.Invite,
|
join_rule: knock ? JoinRule.Knock : JoinRule.Invite,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (access === CreateRoomAccess.Public) {
|
if (kind === CreateRoomKind.Public) {
|
||||||
content = {
|
content = {
|
||||||
join_rule: JoinRule.Public,
|
join_rule: JoinRule.Public,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (access === CreateRoomAccess.Restricted && parent) {
|
if (kind === CreateRoomKind.Restricted && parent) {
|
||||||
content = {
|
content = {
|
||||||
join_rule: knock ? ('knock_restricted' as JoinRule) : JoinRule.Restricted,
|
join_rule: knock ? ('knock_restricted' as JoinRule) : JoinRule.Restricted,
|
||||||
allow: [
|
allow: [
|
||||||
@@ -74,10 +74,6 @@ export const createRoomParentState = (parent: Room) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const createSpacePowerLevelsOverride = () => ({
|
|
||||||
events_default: 50,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createRoomEncryptionState = () => ({
|
export const createRoomEncryptionState = () => ({
|
||||||
type: 'm.room.encryption',
|
type: 'm.room.encryption',
|
||||||
state_key: '',
|
state_key: '',
|
||||||
@@ -86,23 +82,11 @@ export const createRoomEncryptionState = () => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createRoomCallState = () => ({
|
|
||||||
type: 'org.matrix.msc3401.call',
|
|
||||||
state_key: '',
|
|
||||||
content: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createVoiceRoomPowerLevelsOverride = () => ({
|
|
||||||
events: {
|
|
||||||
[StateEvent.GroupCallMemberPrefix]: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type CreateRoomData = {
|
export type CreateRoomData = {
|
||||||
version: string;
|
version: string;
|
||||||
type?: RoomType;
|
type?: RoomType;
|
||||||
parent?: Room;
|
parent?: Room;
|
||||||
access: CreateRoomAccess;
|
kind: CreateRoomKind;
|
||||||
name: string;
|
name: string;
|
||||||
topic?: string;
|
topic?: string;
|
||||||
aliasLocalPart?: string;
|
aliasLocalPart?: string;
|
||||||
@@ -122,11 +106,7 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
|
|||||||
initialState.push(createRoomParentState(data.parent));
|
initialState.push(createRoomParentState(data.parent));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === RoomType.Call) {
|
initialState.push(createRoomJoinRulesState(data.kind, data.parent, data.knock));
|
||||||
initialState.push(createRoomCallState());
|
|
||||||
}
|
|
||||||
|
|
||||||
initialState.push(createRoomJoinRulesState(data.access, data.parent, data.knock));
|
|
||||||
|
|
||||||
const options: ICreateRoomOpts = {
|
const options: ICreateRoomOpts = {
|
||||||
room_version: data.version,
|
room_version: data.version,
|
||||||
@@ -138,15 +118,9 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
|
|||||||
data.allowFederation,
|
data.allowFederation,
|
||||||
data.additionalCreators
|
data.additionalCreators
|
||||||
),
|
),
|
||||||
power_level_content_override:
|
|
||||||
data.type === RoomType.Call ? createVoiceRoomPowerLevelsOverride() : undefined,
|
|
||||||
initial_state: initialState,
|
initial_state: initialState,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data.type === RoomType.Space) {
|
|
||||||
options.power_level_content_override = createSpacePowerLevelsOverride();
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await mx.createRoom(options);
|
const result = await mx.createRoom(options);
|
||||||
|
|
||||||
if (data.parent) {
|
if (data.parent) {
|
||||||
|
|||||||
@@ -88,8 +88,6 @@ export function EmoticonAutocomplete({
|
|||||||
{autoCompleteEmoticon.map((emoticon) => {
|
{autoCompleteEmoticon.map((emoticon) => {
|
||||||
const isCustomEmoji = 'url' in emoticon;
|
const isCustomEmoji = 'url' in emoticon;
|
||||||
const key = isCustomEmoji ? emoticon.url : emoticon.unicode;
|
const key = isCustomEmoji ? emoticon.url : emoticon.unicode;
|
||||||
const customEmojiUrl = mxcUrlToHttp(mx, key, useAuthentication);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={emoticon.shortcode + key}
|
key={emoticon.shortcode + key}
|
||||||
@@ -100,11 +98,11 @@ export function EmoticonAutocomplete({
|
|||||||
}
|
}
|
||||||
onClick={() => handleAutocomplete(key, emoticon.shortcode)}
|
onClick={() => handleAutocomplete(key, emoticon.shortcode)}
|
||||||
before={
|
before={
|
||||||
isCustomEmoji && customEmojiUrl ? (
|
isCustomEmoji ? (
|
||||||
<Box
|
<Box
|
||||||
shrink="No"
|
shrink="No"
|
||||||
as="img"
|
as="img"
|
||||||
src={customEmojiUrl}
|
src={mxcUrlToHttp(mx, key, useAuthentication) || key}
|
||||||
alt={emoticon.shortcode}
|
alt={emoticon.shortcode}
|
||||||
style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }}
|
style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -169,13 +169,12 @@ export function RoomMentionAutocomplete({
|
|||||||
<RoomIcon
|
<RoomIcon
|
||||||
size="50"
|
size="50"
|
||||||
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
|
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
|
||||||
roomType={room.getType()}
|
|
||||||
filled
|
filled
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RoomIcon size="100" joinRule={room.getJoinRule()} roomType={room.getType()} />
|
<RoomIcon size="100" joinRule={room.getJoinRule()} space={room.isSpaceRoom()} />
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,12 +157,10 @@ const getInlineElement = (node: ChildNode, processText: ProcessTextCallback): In
|
|||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
const children = node.childNodes.flatMap((child) => getInlineElement(child, processText));
|
return node.childNodes.flatMap((child) => getInlineElement(child, processText));
|
||||||
if (children.length === 0) return [{ text: '' }];
|
|
||||||
return children;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [{ text: '' }];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseBlockquoteNode = (
|
const parseBlockquoteNode = (
|
||||||
@@ -193,7 +191,7 @@ const parseBlockquoteNode = (
|
|||||||
|
|
||||||
if (child.name === 'p') {
|
if (child.name === 'p') {
|
||||||
appendLine();
|
appendLine();
|
||||||
quoteLines.push(getInlineElement(child, processText));
|
quoteLines.push(child.children.flatMap((c) => getInlineElement(c, processText)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,13 +228,9 @@ const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElemen
|
|||||||
children: [{ text }],
|
children: [{ text }],
|
||||||
}));
|
}));
|
||||||
const childCode = node.children[0];
|
const childCode = node.children[0];
|
||||||
const attribs =
|
const className =
|
||||||
isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs : undefined;
|
isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs.class ?? '' : '';
|
||||||
const languageClass = attribs?.class;
|
const prefix = { text: `${mdSequence}${className.replace('language-', '')}` };
|
||||||
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] },
|
||||||
@@ -255,67 +249,10 @@ const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElemen
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
const parseListNode = (
|
||||||
const parseListMarkdown = (
|
|
||||||
node: Element,
|
node: Element,
|
||||||
processText: ProcessTextCallback,
|
processText: ProcessTextCallback
|
||||||
depth = 0
|
): OrderedListElement[] | UnorderedListElement[] | ParagraphElement[] => {
|
||||||
): ParagraphElement[] => {
|
|
||||||
const md = isTag(node) && node.name === 'ul' ? '*' : '-';
|
|
||||||
const prefix = node.attribs['data-md'] ?? md;
|
|
||||||
const [starOrHyphen] = prefix.match(/^\*|-$/) ?? [];
|
|
||||||
const [digitOrChar] = prefix.match(/^[\da-zA-Z]/) ?? [];
|
|
||||||
|
|
||||||
const digit = digitOrChar ? parseInt(digitOrChar, 10) : undefined;
|
|
||||||
|
|
||||||
const lines: ParagraphElement[] = [];
|
|
||||||
let lineNo = digit === undefined || Number.isNaN(digit) ? digitOrChar ?? 1 : digit;
|
|
||||||
const pushLine = (line: InlineElement[]) => {
|
|
||||||
lines.push({
|
|
||||||
type: BlockType.Paragraph,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
text: `${Array(depth + 1).join(' ')}${starOrHyphen ? `${starOrHyphen} ` : `${lineNo}. `}`,
|
|
||||||
},
|
|
||||||
...line,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
if (typeof lineNo === 'string') {
|
|
||||||
lineNo = String.fromCharCode(lineNo.charCodeAt(0) + 1);
|
|
||||||
} else {
|
|
||||||
lineNo += 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
node.children.forEach((child) => {
|
|
||||||
if (isText(child)) {
|
|
||||||
pushLine([{ text: processText(child.data) }]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTag(child)) {
|
|
||||||
if (child.name === 'ul' || child.name === 'ol') {
|
|
||||||
lines.push(...parseListMarkdown(child, processText, depth + 1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (child.name === 'li') {
|
|
||||||
child.children.forEach((c) => {
|
|
||||||
if (isTag(c) && (c.name === 'ul' || c.name === 'ol')) {
|
|
||||||
lines.push(...parseListMarkdown(c, processText, depth + 1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pushLine(getInlineElement(c, processText));
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pushLine(getInlineElement(child, processText));
|
|
||||||
});
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
};
|
|
||||||
const parseListLines = (children: ChildNode[], processText: ProcessTextCallback) => {
|
|
||||||
const listLines: Array<InlineElement[]> = [];
|
const listLines: Array<InlineElement[]> = [];
|
||||||
let lineHolder: InlineElement[] = [];
|
let lineHolder: InlineElement[] = [];
|
||||||
|
|
||||||
@@ -326,7 +263,7 @@ const parseListLines = (children: ChildNode[], processText: ProcessTextCallback)
|
|||||||
lineHolder = [];
|
lineHolder = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
children.forEach((child) => {
|
node.children.forEach((child) => {
|
||||||
if (isText(child)) {
|
if (isText(child)) {
|
||||||
lineHolder.push({ text: processText(child.data) });
|
lineHolder.push({ text: processText(child.data) });
|
||||||
return;
|
return;
|
||||||
@@ -340,7 +277,7 @@ const parseListLines = (children: ChildNode[], processText: ProcessTextCallback)
|
|||||||
|
|
||||||
if (child.name === 'li') {
|
if (child.name === 'li') {
|
||||||
appendLine();
|
appendLine();
|
||||||
listLines.push(getInlineElement(child, processText));
|
listLines.push(child.children.flatMap((c) => getInlineElement(c, processText)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,23 +286,24 @@ const parseListLines = (children: ChildNode[], processText: ProcessTextCallback)
|
|||||||
});
|
});
|
||||||
appendLine();
|
appendLine();
|
||||||
|
|
||||||
return listLines;
|
const mdSequence = node.attribs['data-md'];
|
||||||
};
|
if (mdSequence !== undefined) {
|
||||||
const parseListNode = (
|
const prefix = mdSequence || '-';
|
||||||
node: Element,
|
const [starOrHyphen] = prefix.match(/^\*|-$/) ?? [];
|
||||||
processText: ProcessTextCallback
|
return listLines.map((lineChildren) => ({
|
||||||
): OrderedListElement[] | UnorderedListElement[] | ParagraphElement[] => {
|
type: BlockType.Paragraph,
|
||||||
if (node.attribs['data-md'] !== undefined) {
|
children: [
|
||||||
return parseListMarkdown(node, processText);
|
{ text: `${starOrHyphen ? `${starOrHyphen} ` : `${prefix}. `} ` },
|
||||||
|
...lineChildren,
|
||||||
|
],
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = parseListLines(node.childNodes, processText);
|
|
||||||
|
|
||||||
if (node.name === 'ol') {
|
if (node.name === 'ol') {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: BlockType.OrderedList,
|
type: BlockType.OrderedList,
|
||||||
children: lines.map((lineChildren) => ({
|
children: listLines.map((lineChildren) => ({
|
||||||
type: BlockType.ListItem,
|
type: BlockType.ListItem,
|
||||||
children: lineChildren,
|
children: lineChildren,
|
||||||
})),
|
})),
|
||||||
@@ -376,7 +314,7 @@ const parseListNode = (
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: BlockType.UnorderedList,
|
type: BlockType.UnorderedList,
|
||||||
children: lines.map((lineChildren) => ({
|
children: listLines.map((lineChildren) => ({
|
||||||
type: BlockType.ListItem,
|
type: BlockType.ListItem,
|
||||||
children: lineChildren,
|
children: lineChildren,
|
||||||
})),
|
})),
|
||||||
@@ -387,7 +325,7 @@ const parseHeadingNode = (
|
|||||||
node: Element,
|
node: Element,
|
||||||
processText: ProcessTextCallback
|
processText: ProcessTextCallback
|
||||||
): HeadingElement | ParagraphElement => {
|
): HeadingElement | ParagraphElement => {
|
||||||
const children = getInlineElement(node, processText);
|
const children = node.children.flatMap((child) => getInlineElement(child, processText));
|
||||||
|
|
||||||
const headingMatch = node.name.match(/^h([123456])$/);
|
const headingMatch = node.name.match(/^h([123456])$/);
|
||||||
const [, g1AsLevel] = headingMatch ?? ['h3', '3'];
|
const [, g1AsLevel] = headingMatch ?? ['h3', '3'];
|
||||||
@@ -450,7 +388,7 @@ export const domToEditorInput = (
|
|||||||
appendLine();
|
appendLine();
|
||||||
children.push({
|
children.push({
|
||||||
type: BlockType.Paragraph,
|
type: BlockType.Paragraph,
|
||||||
children: getInlineElement(node, processText),
|
children: node.children.flatMap((child) => getInlineElement(child, processText)),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from '../../plugins/markdown';
|
} from '../../plugins/markdown';
|
||||||
import { findAndReplace } from '../../utils/findAndReplace';
|
import { findAndReplace } from '../../utils/findAndReplace';
|
||||||
import { sanitizeForRegex } from '../../utils/regex';
|
import { sanitizeForRegex } from '../../utils/regex';
|
||||||
import { isUserId } from '../../utils/matrix';
|
import { getCanonicalAliasOrRoomId, isUserId } from '../../utils/matrix';
|
||||||
|
|
||||||
export type OutputOptions = {
|
export type OutputOptions = {
|
||||||
allowTextFormatting?: boolean;
|
allowTextFormatting?: boolean;
|
||||||
@@ -212,10 +212,9 @@ export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): M
|
|||||||
if (node.type === BlockType.CodeBlock) return;
|
if (node.type === BlockType.CodeBlock) return;
|
||||||
|
|
||||||
if (node.type === BlockType.Mention) {
|
if (node.type === BlockType.Mention) {
|
||||||
if (node.name === '@room') {
|
if (node.id === getCanonicalAliasOrRoomId(mx, roomId)) {
|
||||||
mentionData.room = true;
|
mentionData.room = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUserId(node.id) && node.id !== mx.getUserId()) {
|
if (isUserId(node.id) && node.id !== mx.getUserId()) {
|
||||||
mentionData.users.add(node.id);
|
mentionData.users.add(node.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,7 +202,8 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
|
|||||||
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
|
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined;
|
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ||
|
||||||
|
pack.meta.avatar;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageGroupIcon
|
<ImageGroupIcon
|
||||||
@@ -265,7 +266,7 @@ function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSide
|
|||||||
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
|
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined;
|
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || pack.meta.avatar;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageGroupIcon
|
<ImageGroupIcon
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiIte
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
className={css.CustomEmojiImg}
|
className={css.CustomEmojiImg}
|
||||||
alt={image.body || image.shortcode}
|
alt={image.body || image.shortcode}
|
||||||
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
|
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -98,7 +98,7 @@ export function StickerItem({ mx, useAuthentication, image }: StickerItemProps)
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
className={css.StickerImg}
|
className={css.StickerImg}
|
||||||
alt={image.body || image.shortcode}
|
alt={image.body || image.shortcode}
|
||||||
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
|
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow
|
|||||||
|
|
||||||
const [downloadState, download] = useAsyncCallback(
|
const [downloadState, download] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
||||||
if (!mediaUrl) throw new Error('Invalid media URL');
|
|
||||||
const fileContent = encInfo
|
const fileContent = encInfo
|
||||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
||||||
: await downloadMedia(mediaUrl);
|
: await downloadMedia(mediaUrl);
|
||||||
|
|||||||
@@ -389,30 +389,9 @@ 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 />;
|
|
||||||
|
|
||||||
const lat = parseFloat(location.latitude);
|
|
||||||
const lon = parseFloat(location.longitude);
|
|
||||||
const mapSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.007},${lat - 0.004},${lon + 0.007},${lat + 0.004}&layer=mapnik&marker=${lat},${lon}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" alignItems="Start" gap="200">
|
<Box direction="Column" alignItems="Start" gap="100">
|
||||||
<iframe
|
<Text size="T400">{geoUri}</Text>
|
||||||
title="Location"
|
|
||||||
src={mapSrc}
|
|
||||||
style={{
|
|
||||||
width: '280px',
|
|
||||||
height: '160px',
|
|
||||||
border: '1px solid var(--bg-surface-border)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
display: 'block',
|
|
||||||
}}
|
|
||||||
scrolling="no"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<Text size="T300" style={{ opacity: 0.65 }}>
|
|
||||||
{`${lat.toFixed(5)}, ${lon.toFixed(5)}`}
|
|
||||||
</Text>
|
|
||||||
<Chip
|
<Chip
|
||||||
as="a"
|
as="a"
|
||||||
size="400"
|
size="400"
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const Time = style({
|
||||||
|
fontWeight: 'inherit',
|
||||||
|
flexShrink: 0,
|
||||||
|
});
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import React, { ComponentProps } from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
import { Text, as } from 'folds';
|
import { Text, as } from 'folds';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../utils/time';
|
import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../utils/time';
|
||||||
|
import * as css from './Time.css';
|
||||||
|
|
||||||
export type TimeProps = {
|
export type TimeProps = {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
ts: number;
|
ts: number;
|
||||||
hour24Clock: boolean;
|
hour24Clock: boolean;
|
||||||
dateFormatString: string;
|
dateFormatString: string;
|
||||||
|
inheritPriority?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,7 +25,7 @@ export type TimeProps = {
|
|||||||
* @returns {React.ReactElement} A <Text as="time"> element with the formatted date/time.
|
* @returns {React.ReactElement} A <Text as="time"> element with the formatted date/time.
|
||||||
*/
|
*/
|
||||||
export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
|
export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
|
||||||
({ compact, hour24Clock, dateFormatString, ts, ...props }, ref) => {
|
({ compact, hour24Clock, dateFormatString, ts, inheritPriority, className, ...props }, ref) => {
|
||||||
const formattedTime = timeHourMinute(ts, hour24Clock);
|
const formattedTime = timeHourMinute(ts, hour24Clock);
|
||||||
|
|
||||||
let time = '';
|
let time = '';
|
||||||
@@ -33,11 +36,18 @@ export const Time = as<'span', TimeProps & ComponentProps<typeof Text>>(
|
|||||||
} else if (yesterday(ts)) {
|
} else if (yesterday(ts)) {
|
||||||
time = `Yesterday ${formattedTime}`;
|
time = `Yesterday ${formattedTime}`;
|
||||||
} else {
|
} else {
|
||||||
time = `${timeDayMonYear(ts, dateFormatString)} ${formattedTime}`;
|
time = `${timeDayMonYear(ts, dateFormatString)}, ${formattedTime}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text as="time" style={{ flexShrink: 0 }} size="T200" priority="300" {...props} ref={ref}>
|
<Text
|
||||||
|
as="time"
|
||||||
|
className={classNames(css.Time, className)}
|
||||||
|
size="T200"
|
||||||
|
priority={inheritPriority ? undefined : '300'}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
{time}
|
{time}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ export function AudioContent({
|
|||||||
|
|
||||||
const [srcState, loadSrc] = useAsyncCallback(
|
const [srcState, loadSrc] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
||||||
if (!mediaUrl) throw new Error('Invalid media URL');
|
|
||||||
const fileContent = encInfo
|
const fileContent = encInfo
|
||||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
||||||
: await downloadMedia(mediaUrl);
|
: await downloadMedia(mediaUrl);
|
||||||
|
|||||||
@@ -86,8 +86,7 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
|
|||||||
|
|
||||||
const [textState, loadText] = useAsyncCallback(
|
const [textState, loadText] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
||||||
if (!mediaUrl) throw new Error('Invalid media URL');
|
|
||||||
const fileContent = encInfo
|
const fileContent = encInfo
|
||||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
||||||
: await downloadMedia(mediaUrl);
|
: await downloadMedia(mediaUrl);
|
||||||
@@ -177,8 +176,7 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
|
|||||||
|
|
||||||
const [pdfState, loadPdf] = useAsyncCallback(
|
const [pdfState, loadPdf] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
||||||
if (!mediaUrl) throw new Error('Invalid media URL');
|
|
||||||
const fileContent = encInfo
|
const fileContent = encInfo
|
||||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
||||||
: await downloadMedia(mediaUrl);
|
: await downloadMedia(mediaUrl);
|
||||||
@@ -255,8 +253,7 @@ export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFil
|
|||||||
|
|
||||||
const [downloadState, download] = useAsyncCallback(
|
const [downloadState, download] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
||||||
if (!mediaUrl) throw new Error('Invalid media URL');
|
|
||||||
const fileContent = encInfo
|
const fileContent = encInfo
|
||||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
|
||||||
: await downloadMedia(mediaUrl);
|
: await downloadMedia(mediaUrl);
|
||||||
|
|||||||
@@ -87,8 +87,7 @@ export const ImageContent = as<'div', ImageContentProps>(
|
|||||||
|
|
||||||
const [srcState, loadSrc] = useAsyncCallback(
|
const [srcState, loadSrc] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
||||||
if (!mediaUrl) throw new Error('Invalid media URL');
|
|
||||||
if (encInfo) {
|
if (encInfo) {
|
||||||
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
||||||
decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo)
|
decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo)
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Box, Text } from 'folds';
|
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
|
||||||
|
|
||||||
type PollTextValue = Array<{ body: string }> | string;
|
|
||||||
|
|
||||||
function extractText(val: PollTextValue | undefined): string {
|
|
||||||
if (!val) return '';
|
|
||||||
if (typeof val === 'string') return val;
|
|
||||||
return val[0]?.body ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
type PollAnswer = {
|
|
||||||
'm.id'?: string;
|
|
||||||
id?: string;
|
|
||||||
'm.text'?: PollTextValue;
|
|
||||||
'org.matrix.msc3381.poll.answer'?: { body: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
type PollData = {
|
|
||||||
question?: { body?: string; 'm.text'?: PollTextValue };
|
|
||||||
answers?: PollAnswer[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function PollContent({
|
|
||||||
content,
|
|
||||||
roomId,
|
|
||||||
eventId,
|
|
||||||
}: {
|
|
||||||
content: Record<string, unknown>;
|
|
||||||
roomId?: string;
|
|
||||||
eventId?: string;
|
|
||||||
}) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const [myVote, setMyVote] = useState<string | null>(null);
|
|
||||||
const isStable = !!content['m.poll'];
|
|
||||||
|
|
||||||
const poll = (
|
|
||||||
content['m.poll'] ?? content['org.matrix.msc3381.poll.start']
|
|
||||||
) as PollData | undefined;
|
|
||||||
|
|
||||||
if (!poll) {
|
|
||||||
return (
|
|
||||||
<Text style={{ opacity: 0.6 }}>
|
|
||||||
<i>Poll (unreadable format)</i>
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const questionText =
|
|
||||||
extractText((poll.question as any)?.['m.text']) ||
|
|
||||||
(poll.question as any)?.body ||
|
|
||||||
'Untitled poll';
|
|
||||||
|
|
||||||
const canVote = !!roomId && !!eventId;
|
|
||||||
|
|
||||||
const handleVote = (answerId: string) => {
|
|
||||||
if (!roomId || !eventId) return;
|
|
||||||
setMyVote(answerId);
|
|
||||||
if (isStable) {
|
|
||||||
mx.sendEvent(roomId, 'm.poll.response' as any, {
|
|
||||||
'm.relates_to': { rel_type: 'm.reference', event_id: eventId },
|
|
||||||
'm.responses': [{ 'm.id': answerId }],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
mx.sendEvent(roomId, 'org.matrix.msc3381.poll.response' as any, {
|
|
||||||
'm.relates_to': { rel_type: 'm.reference', event_id: eventId },
|
|
||||||
'org.matrix.msc3381.poll.response': { answers: [answerId] },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
direction="Column"
|
|
||||||
gap="200"
|
|
||||||
style={{ maxWidth: '340px', paddingTop: '2px', paddingBottom: '4px' }}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
alignItems="Center"
|
|
||||||
gap="100"
|
|
||||||
style={{
|
|
||||||
fontSize: '0.68rem',
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: '0.12em',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
opacity: 0.55,
|
|
||||||
marginBottom: '2px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
◉ Poll
|
|
||||||
</Box>
|
|
||||||
<Text size="T400" style={{ fontWeight: 600 }}>
|
|
||||||
{questionText}
|
|
||||||
</Text>
|
|
||||||
<Box direction="Column" gap="100" style={{ marginTop: '2px' }}>
|
|
||||||
{(poll.answers ?? []).map((answer, i) => {
|
|
||||||
const text =
|
|
||||||
extractText((answer as any)['m.text']) ||
|
|
||||||
(answer as any)['org.matrix.msc3381.poll.answer']?.body ||
|
|
||||||
`Option ${i + 1}`;
|
|
||||||
const id = answer['m.id'] ?? answer.id ?? String(i);
|
|
||||||
const selected = myVote === id;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={id}
|
|
||||||
type="button"
|
|
||||||
onClick={canVote ? () => handleVote(id) : undefined}
|
|
||||||
style={{
|
|
||||||
padding: '7px 12px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
background: selected ? 'var(--bg-surface-active)' : 'var(--bg-surface-low)',
|
|
||||||
border: `1px solid ${selected ? 'var(--text-primary)' : 'var(--bg-surface-border)'}`,
|
|
||||||
fontSize: '0.88rem',
|
|
||||||
lineHeight: 1.4,
|
|
||||||
textAlign: 'left',
|
|
||||||
cursor: canVote ? 'pointer' : 'default',
|
|
||||||
color: 'var(--text-primary)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ flexGrow: 1 }}>{text}</span>
|
|
||||||
{selected && (
|
|
||||||
<span style={{ opacity: 0.8, fontSize: '1rem', flexShrink: 0 }}>✓</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
{canVote ? (
|
|
||||||
<Text size="T200" style={{ opacity: 0.5, marginTop: '2px' }}>
|
|
||||||
<i>{myVote ? 'Vote cast — click another to change' : 'Click an option to vote'}</i>
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Text size="T200" style={{ opacity: 0.4, marginTop: '2px' }}>
|
|
||||||
<i>Open in Element to vote</i>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -23,8 +23,7 @@ export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) {
|
|||||||
throw new Error('Failed to load thumbnail');
|
throw new Error('Failed to load thumbnail');
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication);
|
const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? thumbMxcUrl;
|
||||||
if (!mediaUrl) throw new Error('Invalid media URL');
|
|
||||||
if (encInfo) {
|
if (encInfo) {
|
||||||
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
||||||
decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo)
|
decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo)
|
||||||
|
|||||||
@@ -81,8 +81,7 @@ export const VideoContent = as<'div', VideoContentProps>(
|
|||||||
|
|
||||||
const [srcState, loadSrc] = useAsyncCallback(
|
const [srcState, loadSrc] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
||||||
if (!mediaUrl) throw new Error('Invalid media URL');
|
|
||||||
const fileContent = encInfo
|
const fileContent = encInfo
|
||||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
||||||
decryptFile(encBuf, mimeType, encInfo)
|
decryptFile(encBuf, mimeType, encInfo)
|
||||||
|
|||||||
@@ -5,4 +5,3 @@ export * from './AudioContent';
|
|||||||
export * from './FileContent';
|
export * from './FileContent';
|
||||||
export * from './FallbackContent';
|
export * from './FallbackContent';
|
||||||
export * from './EventContent';
|
export * from './EventContent';
|
||||||
export * from './PollContent';
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { JoinRule } from 'matrix-js-sdk';
|
|||||||
import { AvatarFallback, AvatarImage, Icon, Icons, color } from 'folds';
|
import { AvatarFallback, AvatarImage, Icon, Icons, color } from 'folds';
|
||||||
import React, { ComponentProps, ReactEventHandler, ReactNode, forwardRef, useState } from 'react';
|
import React, { ComponentProps, ReactEventHandler, ReactNode, forwardRef, useState } from 'react';
|
||||||
import * as css from './RoomAvatar.css';
|
import * as css from './RoomAvatar.css';
|
||||||
import { getRoomIconSrc } from '../../utils/room';
|
import { joinRuleToIconSrc } from '../../utils/room';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
|
||||||
type RoomAvatarProps = {
|
type RoomAvatarProps = {
|
||||||
@@ -44,9 +44,13 @@ export function RoomAvatar({ roomId, src, alt, renderFallback }: RoomAvatarProps
|
|||||||
export const RoomIcon = forwardRef<
|
export const RoomIcon = forwardRef<
|
||||||
SVGSVGElement,
|
SVGSVGElement,
|
||||||
Omit<ComponentProps<typeof Icon>, 'src'> & {
|
Omit<ComponentProps<typeof Icon>, 'src'> & {
|
||||||
joinRule?: JoinRule;
|
joinRule: JoinRule;
|
||||||
roomType?: string;
|
space?: boolean;
|
||||||
}
|
}
|
||||||
>(({ joinRule, roomType, ...props }, ref) => (
|
>(({ joinRule, space, ...props }, ref) => (
|
||||||
<Icon src={getRoomIconSrc(Icons, roomType, joinRule)} {...props} ref={ref} />
|
<Icon
|
||||||
|
src={joinRuleToIconSrc(Icons, joinRule, space || false) ?? Icons.Hash}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export const SequenceCard = as<
|
|||||||
firstChild,
|
firstChild,
|
||||||
lastChild,
|
lastChild,
|
||||||
outlined,
|
outlined,
|
||||||
mergeBorder,
|
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@@ -25,7 +24,7 @@ export const SequenceCard = as<
|
|||||||
<Box
|
<Box
|
||||||
as={AsSequenceCard}
|
as={AsSequenceCard}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
css.SequenceCard({ radii, outlined, mergeBorder }),
|
css.SequenceCard({ radii, outlined }),
|
||||||
ContainerColor({ variant }),
|
ContainerColor({ variant }),
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const SequenceCard = recipe({
|
|||||||
},
|
},
|
||||||
borderStyle: 'solid',
|
borderStyle: 'solid',
|
||||||
borderWidth: outlinedWidth,
|
borderWidth: outlinedWidth,
|
||||||
|
borderBottomWidth: 0,
|
||||||
selectors: {
|
selectors: {
|
||||||
'&:first-child, :not(&) + &': {
|
'&:first-child, :not(&) + &': {
|
||||||
borderTopLeftRadius: [radii],
|
borderTopLeftRadius: [radii],
|
||||||
@@ -20,6 +20,7 @@ export const SequenceCard = recipe({
|
|||||||
'&:last-child, &:not(:has(+&))': {
|
'&:last-child, &:not(:has(+&))': {
|
||||||
borderBottomLeftRadius: [radii],
|
borderBottomLeftRadius: [radii],
|
||||||
borderBottomRightRadius: [radii],
|
borderBottomRightRadius: [radii],
|
||||||
|
borderBottomWidth: outlinedWidth,
|
||||||
},
|
},
|
||||||
[`&[data-first-child="true"]`]: {
|
[`&[data-first-child="true"]`]: {
|
||||||
borderTopLeftRadius: [radii],
|
borderTopLeftRadius: [radii],
|
||||||
@@ -73,16 +74,6 @@ export const SequenceCard = recipe({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mergeBorder: {
|
|
||||||
true: {
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
selectors: {
|
|
||||||
'&:last-child, &:not(:has(+&))': {
|
|
||||||
borderBottomWidth: outlinedWidth,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
radii: '400',
|
radii: '400',
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function SplashScreen({ children }: SplashScreenProps) {
|
|||||||
justifyContent="Center"
|
justifyContent="Center"
|
||||||
>
|
>
|
||||||
<Text size="H2" align="Center">
|
<Text size="H2" align="Center">
|
||||||
Lotus Chat
|
Cinny
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { as, Avatar } from 'folds';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import * as css from './styles.css';
|
|
||||||
|
|
||||||
type StackedAvatarProps = {
|
|
||||||
radii?: '0' | '300' | '400' | '500' | 'Pill' | 'Inherit' | undefined;
|
|
||||||
};
|
|
||||||
export const StackedAvatar = as<'span', css.StackedAvatarVariants & StackedAvatarProps>(
|
|
||||||
({ size, variant, className, ...props }, ref) => (
|
|
||||||
<Avatar
|
|
||||||
size={size}
|
|
||||||
className={classNames(css.StackedAvatar({ size, variant }), className)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './StackedAvatar';
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { ComplexStyleRule } from '@vanilla-extract/css';
|
|
||||||
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
|
||||||
import { color, config, ContainerColor, toRem } from 'folds';
|
|
||||||
|
|
||||||
const getVariant = (variant: ContainerColor): ComplexStyleRule => ({
|
|
||||||
outlineColor: color[variant].Container,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const StackedAvatar = recipe({
|
|
||||||
base: {
|
|
||||||
backgroundColor: color.Surface.Container,
|
|
||||||
outlineStyle: 'solid',
|
|
||||||
selectors: {
|
|
||||||
'&:first-child': {
|
|
||||||
marginLeft: 0,
|
|
||||||
},
|
|
||||||
'button&': {
|
|
||||||
cursor: 'pointer',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
variants: {
|
|
||||||
size: {
|
|
||||||
'200': {
|
|
||||||
marginLeft: toRem(-6),
|
|
||||||
outlineWidth: config.borderWidth.B300,
|
|
||||||
},
|
|
||||||
'300': {
|
|
||||||
marginLeft: toRem(-9),
|
|
||||||
outlineWidth: config.borderWidth.B400,
|
|
||||||
},
|
|
||||||
'400': {
|
|
||||||
marginLeft: toRem(-10.5),
|
|
||||||
outlineWidth: config.borderWidth.B500,
|
|
||||||
},
|
|
||||||
'500': {
|
|
||||||
marginLeft: toRem(-13),
|
|
||||||
outlineWidth: config.borderWidth.B600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
variant: {
|
|
||||||
Background: getVariant('Background'),
|
|
||||||
Surface: getVariant('Surface'),
|
|
||||||
SurfaceVariant: getVariant('SurfaceVariant'),
|
|
||||||
Primary: getVariant('Primary'),
|
|
||||||
Secondary: getVariant('Secondary'),
|
|
||||||
Success: getVariant('Success'),
|
|
||||||
Warning: getVariant('Warning'),
|
|
||||||
Critical: getVariant('Critical'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
size: '400',
|
|
||||||
variant: 'Surface',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type StackedAvatarVariants = RecipeVariants<typeof StackedAvatar>;
|
|
||||||
@@ -26,12 +26,7 @@ export function SSOStage({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMessage = (evt: MessageEvent) => {
|
const handleMessage = (evt: MessageEvent) => {
|
||||||
if (
|
if (ssoWindow && evt.data === 'authDone' && evt.source === ssoWindow) {
|
||||||
evt.origin === new URL(ssoRedirectURL).origin &&
|
|
||||||
ssoWindow &&
|
|
||||||
evt.data === 'authDone' &&
|
|
||||||
evt.source === ssoWindow
|
|
||||||
) {
|
|
||||||
ssoWindow.close();
|
ssoWindow.close();
|
||||||
setSSOWindow(undefined);
|
setSSOWindow(undefined);
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
@@ -42,7 +37,7 @@ export function SSOStage({
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('message', handleMessage);
|
window.removeEventListener('message', handleMessage);
|
||||||
};
|
};
|
||||||
}, [ssoWindow, handleSubmit, ssoRedirectURL]);
|
}, [ssoWindow, handleSubmit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
|
|||||||
@@ -182,27 +182,6 @@ export function UploadCardRenderer({
|
|||||||
<PreviewVideo fileItem={fileItem} />
|
<PreviewVideo fileItem={fileItem} />
|
||||||
</MediaPreview>
|
</MediaPreview>
|
||||||
)}
|
)}
|
||||||
{(fileItem.originalFile.type.startsWith('image') ||
|
|
||||||
fileItem.originalFile.type.startsWith('video')) && (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Add a caption… (optional)"
|
|
||||||
value={metadata.caption ?? ''}
|
|
||||||
onChange={(e) => setMetadata(fileItem, { ...metadata, caption: e.target.value })}
|
|
||||||
style={{
|
|
||||||
marginTop: '6px',
|
|
||||||
width: '100%',
|
|
||||||
background: 'var(--bg-surface-low)',
|
|
||||||
border: '1px solid var(--bg-surface-border)',
|
|
||||||
borderRadius: '6px',
|
|
||||||
padding: '5px 8px',
|
|
||||||
fontSize: '0.85rem',
|
|
||||||
color: 'var(--text-primary)',
|
|
||||||
outline: 'none',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{upload.status === UploadStatus.Idle && !fileSizeExceeded && (
|
{upload.status === UploadStatus.Idle && !fileSizeExceeded && (
|
||||||
<UploadCardProgress sentBytes={0} totalBytes={file.size} />
|
<UploadCardProgress sentBytes={0} totalBytes={file.size} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -23,11 +23,6 @@ export const UrlPreviewImg = style([
|
|||||||
objectPosition: 'center',
|
objectPosition: 'center',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
cursor: 'pointer',
|
|
||||||
|
|
||||||
':hover': {
|
|
||||||
filter: 'brightness(0.8)',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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';
|
||||||
@@ -13,8 +12,6 @@ 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 };
|
||||||
|
|
||||||
@@ -22,7 +19,6 @@ 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])
|
||||||
);
|
);
|
||||||
@@ -34,41 +30,11 @@ 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 thumbUrl = mxcUrlToHttp(
|
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication, 256, 256, 'scale', false);
|
||||||
mx,
|
|
||||||
prev['og:image'] || '',
|
|
||||||
useAuthentication,
|
|
||||||
256,
|
|
||||||
256,
|
|
||||||
'scale',
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{thumbUrl && (
|
{imgUrl && <UrlPreviewImg src={imgUrl} alt={prev['og:title']} title={prev['og:title']} />}
|
||||||
<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}
|
||||||
@@ -76,7 +42,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
|||||||
as="a"
|
as="a"
|
||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="no-referrer"
|
||||||
size="T200"
|
size="T200"
|
||||||
priority="300"
|
priority="300"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -28,11 +28,7 @@ import { copyToClipboard } from '../../utils/dom';
|
|||||||
import { getExploreServerPath } from '../../pages/pathUtils';
|
import { getExploreServerPath } from '../../pages/pathUtils';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
import { factoryRoomIdByAtoZ } from '../../utils/sort';
|
import { factoryRoomIdByAtoZ } from '../../utils/sort';
|
||||||
import {
|
import { useMutualRooms, useMutualRoomsSupport } from '../../hooks/useMutualRooms';
|
||||||
useMutualRooms,
|
|
||||||
useMutualRoomsSupport,
|
|
||||||
useUnstableMutualRoomsSupport,
|
|
||||||
} from '../../hooks/useMutualRooms';
|
|
||||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||||
import { useDirectRooms } from '../../pages/client/direct/useDirectRooms';
|
import { useDirectRooms } from '../../pages/client/direct/useDirectRooms';
|
||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
@@ -237,9 +233,7 @@ type MutualRoomsData = {
|
|||||||
export function MutualRoomsChip({ userId }: { userId: string }) {
|
export function MutualRoomsChip({ userId }: { userId: string }) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const mutualRoomSupported = useMutualRoomsSupport();
|
const mutualRoomSupported = useMutualRoomsSupport();
|
||||||
const mutualRoomUnstable = useUnstableMutualRoomsSupport();
|
|
||||||
const mutualRoomsState = useMutualRooms(userId);
|
const mutualRoomsState = useMutualRooms(userId);
|
||||||
console.log(mutualRoomSupported, mutualRoomsState);
|
|
||||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||||
const closeUserRoomProfile = useCloseUserRoomProfile();
|
const closeUserRoomProfile = useCloseUserRoomProfile();
|
||||||
const directs = useDirectRooms();
|
const directs = useDirectRooms();
|
||||||
@@ -285,7 +279,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
userId === mx.getSafeUserId() ||
|
userId === mx.getSafeUserId() ||
|
||||||
(!mutualRoomSupported && !mutualRoomUnstable) ||
|
!mutualRoomSupported ||
|
||||||
mutualRoomsState.status === AsyncStatus.Error
|
mutualRoomsState.status === AsyncStatus.Error
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
@@ -329,7 +323,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RoomIcon size="100" joinRule={room.getJoinRule()} roomType={room.getType()} />
|
<RoomIcon size="100" joinRule={room.getJoinRule()} />
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,16 +20,6 @@ export type AutoDiscoveryInfo = Record<string, unknown> & {
|
|||||||
'm.identity_server'?: {
|
'm.identity_server'?: {
|
||||||
base_url: string;
|
base_url: string;
|
||||||
};
|
};
|
||||||
'org.matrix.msc2965.authentication'?: {
|
|
||||||
account?: string;
|
|
||||||
issuer?: string;
|
|
||||||
};
|
|
||||||
'org.matrix.msc4143.rtc_foci'?: [
|
|
||||||
{
|
|
||||||
livekit_service_url: string;
|
|
||||||
type: 'livekit';
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const autoDiscovery = async (
|
export const autoDiscovery = async (
|
||||||
|
|||||||
@@ -291,11 +291,7 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RoomIcon
|
<RoomIcon size="200" joinRule={room.getJoinRule()} />
|
||||||
size="200"
|
|
||||||
joinRule={room.getJoinRule()}
|
|
||||||
roomType={room.getType()}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
|
|||||||