Compare commits

...

43 Commits

Author SHA1 Message Date
kfiven e89b8f7d12 chore(release): 4.12.1 [skip ci] 2026-05-15 07:20:54 +00:00
Krishan 9bc1e7e9ff fix: null edit for another release (#2942) 2026-05-15 17:19:08 +10:00
kfiven c05a6be6f2 chore(release): 4.12.0 [skip ci] 2026-05-15 07:02:05 +00:00
Krishan f7f4a41d61 Revert "chore: Update GITHUB_TOKEN to CLA_PAT in prod workflow" (#2941)
Revert "chore: Update GITHUB_TOKEN to CLA_PAT in prod workflow (#2940)"

This reverts commit 81327678b1.
2026-05-15 16:59:52 +10:00
Krishan 81327678b1 chore: Update GITHUB_TOKEN to CLA_PAT in prod workflow (#2940)
Changed GITHUB_TOKEN secret to CLA_PAT for semantic release due to branch protection.
2026-05-15 16:57:38 +10:00
renovate[bot] bad1fb609a fix(deps): update dependency sanitize-html to v2.17.4 (#2937)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 16:33:13 +10:00
Ajay Bura bef267257a fix: support for stable mutual rooms endpoint (#2939)
* add support for stable mutual rooms endpoint

* add stable mutual room feature check
2026-05-15 16:31:47 +10:00
Ajay Bura 909aa430b8 fix: notification cause crash on android (#2938)
fix notification cause crash on android
2026-05-15 16:30:23 +10:00
Krishan 0b99d85244 docs: Update featured communities in Explore (#2936)
* fix: Update featured communities in Explore

* Add new spaces and rooms to config.json

* Remove #pcapdroid room from configuration

* Update rooms list in config.json

Replaced '#archlinux:archlinux.org' with '#tuwunel:grin.hu' in rooms list.

* Update channel list in config.json
2026-05-14 21:04:30 +10:00
LeaPhant 21bbf4bee0 fix: support audio with ogg filetype (#2924)
fix: ogg audio workaround
2026-05-14 15:16:54 +05:30
Ajay Bura e5e0b96861 feat: Add option to start video call in DM (#2745)
* add option to start video all in DM

* show speaker icon for dm's in call status name

* show call view if call is active in room

* add Atria call ringtone

* update element call and widget api

* add option to start voice/video call in dms

* only show call button if user have permission

* allow call widget to send call notification event

* show incoming call dialog and play sound

* fix call permission checks

* allow option to start call in all rooms

* send notification when starting call in non-voice rooms

* hide header call button from voice rooms

* prevent call join if call not supported and started by other party

* update call menu style

* show call not supported message on incoming call notification

* improve the incoming call layout

* video call with right click without opening menu

* allow call widget to fetch media url

* add webRTC missing error

* improve call permission label

---------

Co-authored-by: Krishan <33421343+kfiven@users.noreply.github.com>
2026-05-14 19:41:12 +10:00
Ajay Bura 02d1001583 feat: allow codeblock plaintext inside codeblock and nested lists markdown (#2930)
* fix crash when editing message with empty trailing heading

* remove unused imports

* allow codeblock plaintext inside codeblock by extending fence count

* allow nested list in markdown
2026-05-14 15:02:54 +10:00
Krishan 64468dfb1b Merge commit from fork
Updated the validation for PR number extraction to ensure it contains only numeric content, and changed the secret used for Netlify authentication.
2026-05-14 13:01:54 +10:00
dependabot[bot] 2864a5e4b8 chore(deps): bump dawidd6/action-download-artifact from 20 to 21 (#2925)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 20 to 21.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](https://github.com/dawidd6/action-download-artifact/compare/8305c0f1062bb0d184d09ef4493ecb9288447732...b6e2e70617bc3265edd6dab6c906732b2f1ae151)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-version: '21'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 15:34:41 +10:00
Ajay Bura 735bc15011 fix: empty heading crash on edit msg (#2929)
* fix crash when editing message with empty trailing heading

* remove unused imports
2026-05-11 22:59:43 +10:00
Shea 341fedd932 fix: edit lists crashing and list rendering issue in Firefox (#2920)
* one liner fix editor

* fix firefox rendering lists

* moved fixes

* moved fixes per ajbura
2026-05-03 22:15:08 +10:00
dependabot[bot] d186d31399 chore(deps): bump actions/setup-node from 6.3.0 to 6.4.0 (#2906)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.3.0 to 6.4.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/53b83947a5a98c8d113130e565377fae1a50d02f...48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 10:12:30 +10:00
James 098684973e fix: do not attempt to join call on doubleclick if missing permissions (#2798)
* fix: do not attempt to join call on doubleclick if missing permissions

* update comment

* export getPowersLevelFromMatrixEvent for usage elsewhere

* only read vc permissions when actually needed instead of reactively
2026-04-16 22:25:53 +10:00
Krishan b107109453 chore: remove package group definitions from renovate config (#2898)
Removed group definitions for Slatejs and Call.
2026-04-16 22:00:39 +10:00
dependabot[bot] a33e8db9a3 chore(deps): bump dawidd6/action-download-artifact from 16 to 20 (#2880)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 16 to 20.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](https://github.com/dawidd6/action-download-artifact/compare/2536c51d3d126276eb39f74d6bc9c72ac6ef30d3...8305c0f1062bb0d184d09ef4493ecb9288447732)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-version: '20'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:36:52 +10:00
dependabot[bot] fb76e3ecb4 chore(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 (#2893)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:33:37 +10:00
dependabot[bot] 3d79293167 chore(deps): bump softprops/action-gh-release from 2.3.3 to 3.0.0 (#2892)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.3 to 3.0.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/6cbd405e2c4e67a21c47fa9e383d020e4e28b836...b4309332981a82ec1c5618f44dd2e27cc8bfbfda)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:33:01 +10:00
dependabot[bot] 74745edcda chore(deps): bump nginx from 1.29.5-alpine to 1.29.8-alpine (#2894)
Bumps nginx from 1.29.5-alpine to 1.29.8-alpine.

---
updated-dependencies:
- dependency-name: nginx
  dependency-version: 1.29.8-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:31:53 +10:00
dependabot[bot] 0812131a97 chore(deps): bump docker/build-push-action from 6.19.2 to 7.1.0 (#2895)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.19.2 to 7.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/10e90e3645eae34f1e60eeb005ba3a3d33f178e8...bcafcacb16a39f128d818304e6c9c0c18556b85f)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:27:58 +10:00
dependabot[bot] 1068bba5c7 chore(deps): bump docker/login-action from 3.7.0 to 4.1.0 (#2879)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.1.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/c94ce9fb468520275223c153574b00df6fe4bcc9...4907a6ddec9925e35a0a9e82d7399ccc52663121)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:26:52 +10:00
DJ Chase 1b5e58a3b4 chore: add matrixrooms.info to directory list (#2844)
* chore: add matrixrooms.info to directory list

matrixrooms.info is a directory of all public Matrix rooms it can find,
regardless of homeserver. It is much larger than the morg directory,
so is more useful as a general search
2026-03-28 17:35:53 +11:00
Krishan acae043f31 chore: make error more useful and understandable (#2859)
* chore: make error more useful and understandable

* chore: use similar wording
2026-03-27 21:22:46 +11:00
ranidspace b4299f8f37 feat: add YYYY-MM-DD (ISO 8601) date format to presets (#2712)
* Add YYYY-MM-DD (ISO 8601) date format to presets

* Fix formatting due to added date format
2026-03-27 21:20:10 +11:00
Krishan b6adac6714 chore: add notice about SDK replacement (#2778) 2026-03-25 12:10:15 +11:00
DJ Chase 1c8f203164 chore: add 'Stickers and Emojis' as featured space (#2842)
* Mention CLA in CONTRIBUTING.md

Closes: #2146

* add: 'Stickers and Emojis' to config.json

Add #stickers-and-emojis:tastytea.de (space) to config.json
2026-03-25 12:09:16 +11:00
Krishan 0c30ece281 fix: remove typo in no rooms UI (#2834) 2026-03-23 16:57:52 +11:00
Krishan 4e559e56d4 chore: group related package update together (#2833) 2026-03-23 16:49:22 +11:00
Krishan 19f28b40ac chore: use private vulnerability disclosure (#2827) 2026-03-22 18:29:09 +11:00
Krishan bcaf43a540 chore: fix link in issue triage template (#2826)
* chore: fix link in issue triage template

* chore: delete .github/PULL_REQUEST_TEMPLATE.md
2026-03-22 18:20:33 +11:00
Krishan 9c7b635e7e chore: add new issue triage discussion template (#2825)
* chore: add new issue triage discussion template

* chore: ask for desktop app version as well

* chore: create preapproved.md
2026-03-22 17:55:53 +11:00
Krishan 65c87dff3a chore: add git author to the sem release (#2815) 2026-03-21 12:07:52 +11:00
Krishan 132a76df27 chore: add semantic release (#2759)
* chore: install deps related to semantic release

* chore: add husky config

* ci: add a script to update version number on new release

* ci: update ci/cd to include semantic release changes

* chore: merge dev to semantic-release
2026-03-19 16:26:25 +11:00
DJ Chase b0954eeddc fix: Mention CLA in CONTRIBUTING.md (#2804)
Mention CLA in CONTRIBUTING.md

Closes: #2146
2026-03-19 11:41:41 +11:00
Ajay Bura 8f1add6059 fix: prevent codeblock filename drop on edit (#2780)
prevent codeblock filename drop on edit
2026-03-15 15:37:14 +11:00
Jan Jurzitza 8a78c9699e feat: allow using filenames in codeblocks (#2455)
Allow using filenames in codeblocks

- If there is a dot in the language name, we instead treat the first line after ``` as the filename and everything after the last dot as the language
- we use a custom "data-label" attribute on the code block to specify the name of the file (so only compatible with cinny from this point onwards)
2026-03-14 18:54:03 +11:00
Krishan 0721b29a2c chore: batch slate related deps (#2775) 2026-03-14 17:22:57 +11:00
Ajay Bura 3d354909d6 fix: hover state on url preview image and make it keyboard friendly (#2777)
add hover state on url preview image and make it keyboard friendly
2026-03-14 17:22:18 +11:00
LeaPhant 7570a84dfd Show image viewer when clicking url preview thumbnail (#2309)
* Show large image overlay when clicking url preview thumbnail

* Move image overlay into its own component

* Move ImageOverlay props into extended type

* Remove export for internal type
2026-03-14 11:04:55 +05:30
59 changed files with 8498 additions and 345 deletions
@@ -0,0 +1,127 @@
labels: ["needs-confirmation"]
body:
- type: markdown #add faqs in future
attributes:
value: |
> [!IMPORTANT]
> Please read through [the Discussion rules](https://github.com/cinnyapp/cinny/discussions/2653) and check for both existing [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
- type: markdown
attributes:
value: "# Issue Details"
- type: textarea
attributes:
label: Issue Description
description: |
Provide a detailed description of the issue. Include relevant information, such as:
- The feature or configuration option you encounter the issue with.
- Screenshots, screen recordings, or other supporting media (as needed).
- If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description.
placeholder: |
When I try to send a message in a room, the message doesn't appear in the timeline.
OR
The application crashes when I click on the settings button.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: |
Describe how you expect Cinny to behave in this situation.
placeholder: |
I expected the message to appear in the room timeline immediately after sending.
OR
The settings panel should open smoothly without any crashes.
validations:
required: true
- type: textarea
attributes:
label: Actual Behavior
description: |
Describe how Cinny actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically.
placeholder: |
The application freezes for 3 seconds and then shows a white screen.
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
description: |
Provide a detailed set of step-by-step instructions for reproducing this issue.
placeholder: |
1. Open Cinny and log in to my account
2. Navigate to the #general room
3. Type a message in the message box
4. Press Enter to send
5. Notice that the message doesn't appear in the timeline
validations:
required: true
- type: textarea
attributes:
label: Environement
description: |
Please provide information about your environment. Include the following:
- OS:
- Browser:
- Cinny Web Version: (app.cinny.in or self hosted)
- Cinny desktop Version: (appimage or deb or flatpak)
- Matrix Homeserver:
placeholder: |
- OS: Windows 11
- Browser: Chrome 120.0.6099.109
- Cinny Web Version: 3.2.0 (app.cinny.in or self hosted)
- Cinny desktop Version: 3.2.0 (appimage or deb or flatpak)
- Matrix Homeserver: matrix.org (Synapse 1.97.0)
render: text
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant Logs
description: |
If applicable, add browser console logs to help explain your problem.
**To get browser console logs:**
- Chrome/Edge: Press F12 → Console tab
- Firefox: Press F12 → Console tab
- Safari: Develop → Show Web Inspector → Console
Please wrap large log outputs in code blocks with triple backticks (```).
placeholder: |
```
Error: Failed to send message
at MessageComposer.sendMessage (composer.js:245)
at HTMLButtonElement.onClick (composer.js:189)
TypeError: Cannot read property 'content' of undefined
at RoomTimeline.render (timeline.js:567)
```
render: shell
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: |
Add any other context about the problem here (e.g., when did this start happening, does it happen on different homeservers, etc.)
placeholder: |
- This started happening after I updated to version 3.2.0
- It only happens in encrypted rooms, not in public rooms
- I've tried on both Firefox and Chrome with the same result
- It works fine on my phone using the same account
- This happens on all homeservers I've tested (matrix.org, mozilla.org)
validations:
required: false
- type: markdown
attributes:
value: |
# User Acknowledgements
> [!TIP]
> Use these links to review the existing Cinny [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc).
- type: checkboxes #add faqs in future
attributes:
label: "I acknowledge that:"
options:
- label: I have searched the Cinny repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
required: true
- label: I have checked the "Preview" tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (` ``` `) on separate lines.
required: true
-57
View File
@@ -1,57 +0,0 @@
name: 🐞 Bug Report
description: Report a bug
body:
- type: markdown
attributes:
value: |
## First of all
1. Please search for [existing issues](https://github.com/ajbura/cinny/issues?q=is%3Aissue) about this problem first.
2. Make sure Cinny is up to date.
3. Make sure it's an issue with Cinny and not something else you are using.
4. Remember to be friendly.
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear description of what the bug is. Include screenshots if applicable.
placeholder: Bug description
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Steps to reproduce the behavior.
placeholder: |
1. Go to ...
2. Click on ...
3. See error
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear description of what you expected to happen.
- type: textarea
id: info
attributes:
label: Platform and versions
description: "Provide OS, browser and Cinny version with your Homeserver."
placeholder: |
1. OS: [e.g. Windows 10, MacOS]
2. Browser: [e.g. chrome 99.5, firefox 97.2]
3. Cinny version: [e.g. 1.8.1 (app.cinny.in)]
4. Matrix homeserver: [e.g. matrix.org]
render: shell
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
+4 -3
View File
@@ -1,4 +1,5 @@
blank_issues_enabled: false
contact_links: contact_links:
- name: 💬 Matrix Chat - name: Features, Bug Reports, Questions
url: https://matrix.to/#/#cinny:matrix.org url: https://github.com/cinnyapp/cinny/discussions/new/choose
about: Ask questions and talk to other Cinny users and the maintainers about: Our preferred starting point if you have any questions or suggestions about features or behavior.
@@ -1,33 +0,0 @@
name: 💡 Feature Request
description: Suggest an idea
body:
- type: textarea
id: problem
attributes:
label: Describe the problem
description: A clear description of the problem this feature would solve
placeholder: "I'm always frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like"
description: A clear description of what change you would like
placeholder: "I would like to..."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: "Any alternative solutions you've considered"
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
+9
View File
@@ -0,0 +1,9 @@
---
name: Pre-Discussed and Approved Topics
about: |-
Only for topics already discussed and approved in the GitHub Discussions section.
---
**DO NOT OPEN A NEW ISSUE. PLEASE USE THE DISCUSSIONS SECTION.**
**I DIDN'T READ THE ABOVE LINE. PLEASE CLOSE THIS ISSUE.**
-22
View File
@@ -1,22 +0,0 @@
<!-- Please read https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
### Description
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes #
#### Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
### Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
-3
View File
@@ -1,3 +0,0 @@
# Reporting a Vulnerability
**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**
+9 -1
View File
@@ -3,12 +3,20 @@
"extends": [ "extends": [
"config:recommended", "config:recommended",
":dependencyDashboardApproval", ":dependencyDashboardApproval",
":semanticCommits" ":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": {
+3 -3
View File
@@ -14,7 +14,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup node - name: Setup node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: ".node-version" node-version-file: ".node-version"
package-manager-cache: false package-manager-cache: false
@@ -25,7 +25,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096' NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build run: npm run build
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: preview name: preview
path: dist path: dist
@@ -33,7 +33,7 @@ jobs:
- name: Save pr number - name: Save pr number
run: echo ${PR_NUMBER} > ./pr.txt run: echo ${PR_NUMBER} > ./pr.txt
- name: Upload pr number - name: Upload pr number
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: pr name: pr
path: ./pr.txt path: ./pr.txt
+11 -5
View File
@@ -16,16 +16,22 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }} if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps: steps:
- name: Download pr number - name: Download pr number
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16 uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
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: Output pr number - name: Validate and output pr number
id: pr id: pr
run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT run: |
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@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16 uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
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 }}
@@ -42,7 +48,7 @@ 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 }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN_PR }}
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
+3 -3
View File
@@ -26,7 +26,7 @@ jobs:
- name: Login to Docker Hub #Do not update this action from a outside PR - name: Login to Docker Hub #Do not update this action from a outside PR
if: github.event.pull_request.head.repo.fork == false if: github.event.pull_request.head.repo.fork == false
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
@@ -34,7 +34,7 @@ jobs:
- name: Login to the Github Container registry #Do not update this action from a outside PR - 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 if: github.event.pull_request.head.repo.fork == false
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -50,7 +50,7 @@ jobs:
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
- name: Build Docker image (no push) - name: Build Docker image (no push)
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup node - name: Setup node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: ".node-version" node-version-file: ".node-version"
package-manager-cache: false package-manager-cache: false
+34 -12
View File
@@ -1,23 +1,39 @@
name: Production deploy name: Production deploy
on: on:
release: workflow_dispatch:
types: [published]
jobs: jobs:
deploy-and-tarball: deploy-and-tarball:
name: Netlify deploy and tarball name: Netlify deploy and tarball
outputs:
version: ${{ steps.vars.outputs.tag }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup node - name: Setup node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: ".node-version" node-version-file: ".node-version"
package-manager-cache: false package-manager-cache: false
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Run semantic release
run: npm run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
GIT_COMMITTER_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: Get version from tag
id: vars
run: |
TAG=$(git describe --tags --abbrev=0)
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: Build app - name: Build app
env: env:
NODE_OPTIONS: '--max_old_space_size=4096' NODE_OPTIONS: '--max_old_space_size=4096'
@@ -26,7 +42,7 @@ jobs:
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0 uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
with: with:
publish-dir: dist publish-dir: dist
deploy-message: 'Prod deploy ${{ github.ref_name }}' deploy-message: 'Prod deploy ${{ steps.vars.outputs.tag }}'
enable-commit-comment: false enable-commit-comment: false
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
production-deploy: true production-deploy: true
@@ -36,9 +52,6 @@ jobs:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }}
timeout-minutes: 1 timeout-minutes: 1
- name: Get version from tag
id: vars
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
- name: Create tar.gz - name: Create tar.gz
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
- name: Sign tar.gz - name: Sign tar.gz
@@ -52,14 +65,18 @@ jobs:
gpg --export | xxd -p gpg --export | xxd -p
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
- name: Upload tagged release - name: Upload tagged release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with: with:
tag_name: ${{ steps.vars.outputs.tag }}
files: | files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz cinny-${{ steps.vars.outputs.tag }}.tar.gz
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
publish-image: publish-image:
name: Push Docker image to Docker Hub, GHCR name: Push Docker image to Docker Hub, GHCR
needs: deploy-and-tarball
env:
VERSION: ${{ needs.deploy-and-tarball.outputs.version }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
@@ -67,17 +84,19 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.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@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to Docker Hub #Do not update this action from a outside PR - name: Login to Docker Hub #Do not update this action from a outside PR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to the Github Container registry #Do not update this action from a outside PR - name: Login to the Github Container registry #Do not update this action from a outside PR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -89,11 +108,14 @@ jobs:
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@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
+3
View File
@@ -0,0 +1,3 @@
# These are commented until we enable lint and typecheck
# npx tsc -p tsconfig.json --noEmit
# npx lint-staged
+1 -1
View File
@@ -18,7 +18,7 @@ Bug reports and feature suggestions must use descriptive and concise titles and
## Pull requests ## Pull requests
> ### Legal Notice > ### Legal Notice
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. You will also be asked to [sign the CLA](https://github.com/cinnyapp/cla) upon submiting your pull request.
**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap. **NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.
+1 -1
View File
@@ -11,7 +11,7 @@ RUN npm run build
## App ## App
FROM nginx:1.29.5-alpine FROM nginx:1.29.8-alpine
COPY --from=builder /src/dist /app COPY --from=builder /src/dist /app
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
+4
View File
@@ -16,6 +16,10 @@ A Matrix client focusing primarily on simple, elegant and secure interface. The
- [Roadmap](https://github.com/orgs/cinnyapp/projects/1) - [Roadmap](https://github.com/orgs/cinnyapp/projects/1)
- [Contributing](./CONTRIBUTING.md) - [Contributing](./CONTRIBUTING.md)
> [!IMPORTANT]
We are currently in the [process of replacing the matrix-js-sdk](https://github.com/cinnyapp/cinny/issues/257#issuecomment-3714406704) with our own SDK. As a result, we will not be accepting any pull requests until further notice.
Thank you for your understanding.
<img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380"> <img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
## Getting started ## Getting started
+7 -7
View File
@@ -6,22 +6,22 @@
"featuredCommunities": { "featuredCommunities": {
"openAsDefault": false, "openAsDefault": false,
"spaces": [ "spaces": [
"#cinny-space:matrix.org", "#cinny:matrix.org",
"#community:matrix.org", "#community:matrix.org",
"#space:unredacted.org", "#space:unredacted.org",
"#librewolf-community:matrix.org",
"#stickers-and-emojis:tastytea.de",
"#videogames:waywardinn.com",
"#science-space:matrix.org", "#science-space:matrix.org",
"#libregaming-games:tchncs.de", "#libregaming-games:tchncs.de",
"#mathematics-on:matrix.org" "#mathematics-on:matrix.org"
], ],
"rooms": [ "rooms": [
"#cinny:matrix.org", "#tuwunel:grin.hu",
"#freesoftware:matrix.org", "#freesoftware:matrix.org",
"#pcapdroid:matrix.org", "#gentoo:matrix.org"
"#gentoo:matrix.org",
"#PrivSec.dev:arcticfoxes.net",
"#disroot:aria-net.org"
], ],
"servers": ["matrix.org", "mozilla.org", "unredacted.org"] "servers": ["matrixrooms.info", "matrix.org", "mozilla.org", "unredacted.org"]
}, },
"hashRouter": { "hashRouter": {
+7113 -41
View File
File diff suppressed because it is too large Load Diff
+54 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "cinny", "name": "cinny",
"version": "4.11.1", "version": "4.12.1",
"description": "Yet another matrix client", "description": "Yet another matrix client",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@@ -11,11 +11,52 @@
"start": "vite", "start": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "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",
@@ -56,7 +97,7 @@
"linkify-react": "4.3.2", "linkify-react": "4.3.2",
"linkifyjs": "4.3.2", "linkifyjs": "4.3.2",
"matrix-js-sdk": "38.2.0", "matrix-js-sdk": "38.2.0",
"matrix-widget-api": "1.13.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",
@@ -70,7 +111,7 @@
"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.30.3",
"sanitize-html": "2.12.1", "sanitize-html": "2.17.4",
"slate": "0.123.0", "slate": "0.123.0",
"slate-dom": "0.123.0", "slate-dom": "0.123.0",
"slate-history": "0.113.1", "slate-history": "0.113.1",
@@ -78,10 +119,12 @@
"ua-parser-js": "1.0.35" "ua-parser-js": "1.0.35"
}, },
"devDependencies": { "devDependencies": {
"@element-hq/element-call-embedded": "0.16.3", "@element-hq/element-call-embedded": "0.19.1",
"@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",
@@ -90,12 +133,13 @@
"@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.9.0", "@types/sanitize-html": "2.16.1",
"@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",
@@ -103,7 +147,10 @@
"eslint-plugin-jsx-a11y": "6.6.1", "eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-react": "7.31.11", "eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-hooks": "4.6.0",
"husky": "9.1.7",
"lint-staged": "16.3.2",
"prettier": "2.8.1", "prettier": "2.8.1",
"semantic-release": "25.0.3",
"typescript": "4.9.4", "typescript": "4.9.4",
"vite": "5.4.19", "vite": "5.4.19",
"vite-plugin-pwa": "0.20.5", "vite-plugin-pwa": "0.20.5",
Binary file not shown.
+48
View File
@@ -0,0 +1,48 @@
import fs from "fs";
import path from "path";
import { execSync } from "child_process";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const version = process.argv[2];
if (!version) {
console.error("Version argument missing");
process.exit(1);
}
const root = path.resolve(__dirname, "..");
const newVersionTag = `v${version}`;
// Update package.json + package-lock.json safely
execSync(`npm version ${version} --no-git-tag-version`, {
cwd: root,
stdio: "inherit",
});
console.log(`Updated package.json and package-lock.json → ${version}`);
// Update UI version references
const files = [
"src/app/features/settings/about/About.tsx",
"src/app/pages/auth/AuthFooter.tsx",
"src/app/pages/client/WelcomePage.tsx",
];
files.forEach((filePath) => {
const absPath = path.join(root, filePath);
if (!fs.existsSync(absPath)) {
console.warn(`File not found: ${filePath}`);
return;
}
const content = fs.readFileSync(absPath, "utf8");
const updated = content.replace(/v\d+\.\d+\.\d+/g, newVersionTag);
fs.writeFileSync(absPath, updated);
console.log(`Updated ${filePath}${newVersionTag}`);
});
+337 -3
View File
@@ -1,6 +1,32 @@
import React, { ReactNode, useCallback, useRef } from 'react'; /* eslint-disable jsx-a11y/media-has-caption */
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import { config } from 'folds'; 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 { import {
CallEmbedContextProvider, CallEmbedContextProvider,
CallEmbedRefContextProvider, CallEmbedRefContextProvider,
@@ -8,11 +34,316 @@ import {
useCallJoined, useCallJoined,
useCallThemeSync, useCallThemeSync,
useCallMemberSoundSync, useCallMemberSoundSync,
useCallStart,
} from '../hooks/useCallEmbed'; } from '../hooks/useCallEmbed';
import { callChatAtom, callEmbedAtom } from '../state/callEmbed'; import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
import { CallEmbed } from '../plugins/call'; import { CallEmbed } from '../plugins/call';
import { useSelectedRoom } from '../hooks/router/useSelectedRoom'; import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
import { useMatrixClient } from '../hooks/useMatrixClient';
import CallSound from '../../../public/sound/call.ogg';
import { useCallMembersChange, useCallSession } from '../hooks/useCall';
import { useRoomAvatar, useRoomName } from '../hooks/useRoomMeta';
import { mDirectAtom } from '../state/mDirectList';
import { useMediaAuthentication } from '../hooks/useMediaAuthentication';
import { mxcUrlToHttp } from '../utils/matrix';
import { RoomAvatar, RoomIcon } from './room-avatar';
import { useRoomNavigate } from '../hooks/useRoomNavigate';
import { getStateEvent } from '../utils/room';
import { StateEvent } from '../../types/matrix/room';
import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels';
import { getRoomCreatorsForRoomId } from '../hooks/useRoomCreators';
import { getRoomPermissionsAPI } from '../hooks/useRoomPermissions';
import { useLivekitSupport } from '../hooks/useLivekitSupport';
import { CallAvatarAnimation } from '../styles/Animations.css';
import { webRTCSupported } from '../utils/rtc';
type IncomingCallInfo = {
room: Room;
sender: string;
senderTs: number;
lifetime: number;
intent?: string;
notificationType: RTCNotificationType;
refEventId: string;
};
type IncomingCallProps = {
dm: boolean;
info: IncomingCallInfo;
onIgnore: () => void;
onAnswer: (room: Room, video: boolean) => void;
onReject: (room: Room, eventId: string) => void;
};
function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const livekitSupported = useLivekitSupport();
const rtcSupported = webRTCSupported();
const canAnswer = livekitSupported && rtcSupported;
const { room } = info;
const audioRef = useRef<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 }) { function CallUtils({ embed }: { embed: CallEmbed }) {
const setCallEmbed = useSetAtom(callEmbedAtom); const setCallEmbed = useSetAtom(callEmbedAtom);
@@ -48,7 +379,10 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
return ( return (
<CallEmbedContextProvider value={callEmbed}> <CallEmbedContextProvider value={callEmbed}>
{callEmbed && <CallUtils embed={callEmbed} />} {callEmbed && <CallUtils embed={callEmbed} />}
<CallEmbedRefContextProvider value={callEmbedRef}>{children}</CallEmbedRefContextProvider> <CallEmbedRefContextProvider value={callEmbedRef}>
<IncomingCallListener callEmbed={callEmbed} joined={joined} />
{children}
</CallEmbedRefContextProvider>
<div <div
data-call-embed-container data-call-embed-container
style={{ style={{
+45
View File
@@ -0,0 +1,45 @@
import FocusTrap from 'focus-trap-react';
import { as, Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
import React, { ReactNode } from 'react';
import { ModalWide } from '../styles/Modal.css';
import { stopPropagation } from '../utils/keyboard';
export type RenderViewerProps = {
src: string;
alt: string;
requestClose: () => void;
};
type ImageOverlayProps = RenderViewerProps & {
viewer: boolean;
renderViewer: (props: RenderViewerProps) => ReactNode;
};
export const ImageOverlay = as<'div', ImageOverlayProps>(
({ src, alt, viewer, requestClose, renderViewer, ...props }, ref) => (
<Overlay {...props} ref={ref} open={viewer} backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => requestClose(),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal
className={ModalWide}
size="500"
onContextMenu={(evt: any) => evt.stopPropagation()}
>
{renderViewer({
src,
alt,
requestClose,
})}
</Modal>
</FocusTrap>
</OverlayCenter>
</Overlay>
)
);
+88 -26
View File
@@ -157,10 +157,12 @@ const getInlineElement = (node: ChildNode, processText: ProcessTextCallback): In
return children; return children;
} }
return node.childNodes.flatMap((child) => getInlineElement(child, processText)); const children = node.childNodes.flatMap((child) => getInlineElement(child, processText));
if (children.length === 0) return [{ text: '' }];
return children;
} }
return []; return [{ text: '' }];
}; };
const parseBlockquoteNode = ( const parseBlockquoteNode = (
@@ -191,7 +193,7 @@ const parseBlockquoteNode = (
if (child.name === 'p') { if (child.name === 'p') {
appendLine(); appendLine();
quoteLines.push(child.children.flatMap((c) => getInlineElement(c, processText))); quoteLines.push(getInlineElement(child, processText));
return; return;
} }
@@ -228,9 +230,13 @@ const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElemen
children: [{ text }], children: [{ text }],
})); }));
const childCode = node.children[0]; const childCode = node.children[0];
const className = const attribs =
isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs.class ?? '' : ''; isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs : undefined;
const prefix = { text: `${mdSequence}${className.replace('language-', '')}` }; const languageClass = attribs?.class;
const customLabel = attribs?.['data-label'];
const prefix = {
text: `${mdSequence}${customLabel ?? languageClass?.replace('language-', '') ?? ''}`,
};
const suffix = { text: mdSequence }; const suffix = { text: mdSequence };
return [ return [
{ type: BlockType.Paragraph, children: [prefix] }, { type: BlockType.Paragraph, children: [prefix] },
@@ -249,10 +255,67 @@ const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElemen
}, },
]; ];
}; };
const parseListNode = (
const parseListMarkdown = (
node: Element, node: Element,
processText: ProcessTextCallback processText: ProcessTextCallback,
): OrderedListElement[] | UnorderedListElement[] | ParagraphElement[] => { depth = 0
): ParagraphElement[] => {
const md = isTag(node) && node.name === 'ul' ? '*' : '-';
const prefix = node.attribs['data-md'] ?? md;
const [starOrHyphen] = prefix.match(/^\*|-$/) ?? [];
const [digitOrChar] = prefix.match(/^[\da-zA-Z]/) ?? [];
const digit = digitOrChar ? parseInt(digitOrChar, 10) : undefined;
const lines: ParagraphElement[] = [];
let lineNo = digit === undefined || Number.isNaN(digit) ? digitOrChar ?? 1 : digit;
const pushLine = (line: InlineElement[]) => {
lines.push({
type: BlockType.Paragraph,
children: [
{
text: `${Array(depth + 1).join(' ')}${starOrHyphen ? `${starOrHyphen} ` : `${lineNo}. `}`,
},
...line,
],
});
if (typeof lineNo === 'string') {
lineNo = String.fromCharCode(lineNo.charCodeAt(0) + 1);
} else {
lineNo += 1;
}
};
node.children.forEach((child) => {
if (isText(child)) {
pushLine([{ text: processText(child.data) }]);
return;
}
if (isTag(child)) {
if (child.name === 'ul' || child.name === 'ol') {
lines.push(...parseListMarkdown(child, processText, depth + 1));
return;
}
if (child.name === 'li') {
child.children.forEach((c) => {
if (isTag(c) && (c.name === 'ul' || c.name === 'ol')) {
lines.push(...parseListMarkdown(c, processText, depth + 1));
return;
}
pushLine(getInlineElement(c, processText));
});
return;
}
}
pushLine(getInlineElement(child, processText));
});
return lines;
};
const parseListLines = (children: ChildNode[], processText: ProcessTextCallback) => {
const listLines: Array<InlineElement[]> = []; const listLines: Array<InlineElement[]> = [];
let lineHolder: InlineElement[] = []; let lineHolder: InlineElement[] = [];
@@ -263,7 +326,7 @@ const parseListNode = (
lineHolder = []; lineHolder = [];
}; };
node.children.forEach((child) => { children.forEach((child) => {
if (isText(child)) { if (isText(child)) {
lineHolder.push({ text: processText(child.data) }); lineHolder.push({ text: processText(child.data) });
return; return;
@@ -277,7 +340,7 @@ const parseListNode = (
if (child.name === 'li') { if (child.name === 'li') {
appendLine(); appendLine();
listLines.push(child.children.flatMap((c) => getInlineElement(c, processText))); listLines.push(getInlineElement(child, processText));
return; return;
} }
@@ -286,24 +349,23 @@ const parseListNode = (
}); });
appendLine(); appendLine();
const mdSequence = node.attribs['data-md']; return listLines;
if (mdSequence !== undefined) { };
const prefix = mdSequence || '-'; const parseListNode = (
const [starOrHyphen] = prefix.match(/^\*|-$/) ?? []; node: Element,
return listLines.map((lineChildren) => ({ processText: ProcessTextCallback
type: BlockType.Paragraph, ): OrderedListElement[] | UnorderedListElement[] | ParagraphElement[] => {
children: [ if (node.attribs['data-md'] !== undefined) {
{ text: `${starOrHyphen ? `${starOrHyphen} ` : `${prefix}. `} ` }, return parseListMarkdown(node, processText);
...lineChildren,
],
}));
} }
const lines = parseListLines(node.childNodes, processText);
if (node.name === 'ol') { if (node.name === 'ol') {
return [ return [
{ {
type: BlockType.OrderedList, type: BlockType.OrderedList,
children: listLines.map((lineChildren) => ({ children: lines.map((lineChildren) => ({
type: BlockType.ListItem, type: BlockType.ListItem,
children: lineChildren, children: lineChildren,
})), })),
@@ -314,7 +376,7 @@ const parseListNode = (
return [ return [
{ {
type: BlockType.UnorderedList, type: BlockType.UnorderedList,
children: listLines.map((lineChildren) => ({ children: lines.map((lineChildren) => ({
type: BlockType.ListItem, type: BlockType.ListItem,
children: lineChildren, children: lineChildren,
})), })),
@@ -325,7 +387,7 @@ const parseHeadingNode = (
node: Element, node: Element,
processText: ProcessTextCallback processText: ProcessTextCallback
): HeadingElement | ParagraphElement => { ): HeadingElement | ParagraphElement => {
const children = node.children.flatMap((child) => getInlineElement(child, processText)); const children = getInlineElement(node, 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'];
@@ -388,7 +450,7 @@ export const domToEditorInput = (
appendLine(); appendLine();
children.push({ children.push({
type: BlockType.Paragraph, type: BlockType.Paragraph,
children: node.children.flatMap((child) => getInlineElement(child, processText)), children: getInlineElement(node, processText),
}); });
return; return;
} }
+2 -2
View File
@@ -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 { getCanonicalAliasOrRoomId, isUserId } from '../../utils/matrix'; import { isUserId } from '../../utils/matrix';
export type OutputOptions = { export type OutputOptions = {
allowTextFormatting?: boolean; allowTextFormatting?: boolean;
@@ -215,7 +215,7 @@ export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): M
if (node.name === '@room') { if (node.name === '@room') {
mentionData.room = true; mentionData.room = true;
} }
if (isUserId(node.id) && node.id !== mx.getUserId()) { if (isUserId(node.id) && node.id !== mx.getUserId()) {
mentionData.users.add(node.id); mentionData.users.add(node.id);
} }
@@ -23,6 +23,11 @@ export const UrlPreviewImg = style([
objectPosition: 'center', objectPosition: 'center',
flexShrink: 0, flexShrink: 0,
overflow: 'hidden', overflow: 'hidden',
cursor: 'pointer',
':hover': {
filter: 'brightness(0.8)',
},
}, },
]); ]);
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { IPreviewUrlResponse } from 'matrix-js-sdk'; import { IPreviewUrlResponse } from 'matrix-js-sdk';
import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds'; import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds';
import { ImageOverlay } from '../ImageOverlay';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview'; import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview';
@@ -12,6 +13,8 @@ import * as css from './UrlPreviewCard.css';
import { tryDecodeURIComponent } from '../../utils/dom'; import { tryDecodeURIComponent } from '../../utils/dom';
import { mxcUrlToHttp } from '../../utils/matrix'; import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { ImageViewer } from '../image-viewer';
import { onEnterOrSpace } from '../../utils/keyboard';
const linkStyles = { color: color.Success.Main }; const linkStyles = { color: color.Success.Main };
@@ -19,6 +22,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
({ url, ts, ...props }, ref) => { ({ url, ts, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const [viewer, setViewer] = useState(false);
const [previewStatus, loadPreview] = useAsyncCallback( const [previewStatus, loadPreview] = useAsyncCallback(
useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx]) useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
); );
@@ -30,7 +34,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
if (previewStatus.status === AsyncStatus.Error) return null; if (previewStatus.status === AsyncStatus.Error) return null;
const renderContent = (prev: IPreviewUrlResponse) => { const renderContent = (prev: IPreviewUrlResponse) => {
const imgUrl = mxcUrlToHttp( const thumbUrl = mxcUrlToHttp(
mx, mx,
prev['og:image'] || '', prev['og:image'] || '',
useAuthentication, useAuthentication,
@@ -40,9 +44,31 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
false false
); );
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication);
return ( return (
<> <>
{imgUrl && <UrlPreviewImg src={imgUrl} alt={prev['og:title']} title={prev['og:title']} />} {thumbUrl && (
<UrlPreviewImg
src={thumbUrl}
alt={prev['og:title']}
title={prev['og:title']}
tabIndex={0}
onKeyDown={(evt) => onEnterOrSpace(() => setViewer(true))(evt)}
onClick={() => setViewer(true)}
/>
)}
{imgUrl && (
<ImageOverlay
src={imgUrl}
alt={prev['og:title']}
viewer={viewer}
requestClose={() => {
setViewer(false);
}}
renderViewer={(p) => <ImageViewer {...p} />}
/>
)}
<UrlPreviewContent> <UrlPreviewContent>
<Text <Text
style={linkStyles} style={linkStyles}
@@ -28,7 +28,11 @@ 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 { useMutualRooms, useMutualRoomsSupport } from '../../hooks/useMutualRooms'; import {
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';
@@ -233,7 +237,9 @@ 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();
@@ -279,7 +285,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) {
if ( if (
userId === mx.getSafeUserId() || userId === mx.getSafeUserId() ||
!mutualRoomSupported || (!mutualRoomSupported && !mutualRoomUnstable) ||
mutualRoomsState.status === AsyncStatus.Error mutualRoomsState.status === AsyncStatus.Error
) { ) {
return null; return null;
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Room } from 'matrix-js-sdk'; import { Room } from 'matrix-js-sdk';
import { Chip, Text } from 'folds'; import { Chip, Icon, Icons, Text } from 'folds';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useRoomName } from '../../hooks/useRoomMeta'; import { useRoomName } from '../../hooks/useRoomMeta';
import { RoomIcon } from '../../components/room-avatar'; import { RoomIcon } from '../../components/room-avatar';
@@ -38,7 +38,11 @@ export function CallRoomName({ room }: CallRoomNameProps) {
variant="Background" variant="Background"
radii="Pill" radii="Pill"
before={ before={
<RoomIcon size="200" joinRule={room.getJoinRule()} roomType={room.getType()} filled /> dm ? (
<Icon size="200" src={Icons.VolumeHigh} filled />
) : (
<RoomIcon size="200" joinRule={room.getJoinRule()} roomType={room.getType()} filled />
)
} }
onClick={() => navigateRoom(room.roomId)} onClick={() => navigateRoom(room.roomId)}
> >
+28 -5
View File
@@ -14,11 +14,20 @@ import { CallMemberRenderer } from './CallMemberCard';
import * as css from './styles.css'; import * as css from './styles.css';
import { CallControls } from './CallControls'; import { CallControls } from './CallControls';
import { useLivekitSupport } from '../../hooks/useLivekitSupport'; import { useLivekitSupport } from '../../hooks/useLivekitSupport';
import { webRTCSupported } from '../../utils/rtc';
function LivekitServerMissingMessage() { function LivekitServerMissingMessage() {
return ( return (
<Text style={{ margin: 'auto', color: color.Critical.Main }} size="L400" align="Center"> <Text style={{ margin: 'auto', color: color.Critical.Main }} size="L400" align="Center">
Your homeserver does not support calling. But you can still join call started by others. Your homeserver does not support calling.
</Text>
);
}
function WebRTCMissingError() {
return (
<Text style={{ margin: 'auto', color: color.Critical.Main }} size="L400" align="Center">
Your browser does not support WebRTC, which is required for calling.
</Text> </Text>
); );
} }
@@ -26,16 +35,22 @@ function LivekitServerMissingMessage() {
function JoinMessage({ function JoinMessage({
hasParticipant, hasParticipant,
livekitSupported, livekitSupported,
rtcSupported,
}: { }: {
hasParticipant?: boolean; hasParticipant?: boolean;
livekitSupported?: boolean; livekitSupported?: boolean;
rtcSupported?: boolean;
}) { }) {
if (hasParticipant) return null; if (rtcSupported === false) {
return <WebRTCMissingError />;
}
if (livekitSupported === false) { if (livekitSupported === false) {
return <LivekitServerMissingMessage />; return <LivekitServerMissingMessage />;
} }
if (hasParticipant) return null;
return ( return (
<Text style={{ margin: 'auto' }} size="L400" align="Center"> <Text style={{ margin: 'auto' }} size="L400" align="Center">
Voice chats empty Be the first to hop in! Voice chats empty Be the first to hop in!
@@ -63,12 +78,16 @@ function CallPrescreen() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = useRoom(); const room = useRoom();
const livekitSupported = useLivekitSupport(); const livekitSupported = useLivekitSupport();
const rtcSupported = webRTCSupported();
const powerLevels = usePowerLevelsContext(); const powerLevels = usePowerLevelsContext();
const creators = useRoomCreators(room); const creators = useRoomCreators(room);
const permissions = useRoomPermissions(creators, powerLevels); const permissions = useRoomPermissions(creators, powerLevels);
const hasPermission = permissions.event(StateEvent.GroupCallMemberPrefix, mx.getSafeUserId()); const hasPermission = permissions.stateEvent(
StateEvent.GroupCallMemberPrefix,
mx.getSafeUserId()
);
const callSession = useCallSession(room); const callSession = useCallSession(room);
const callMembers = useCallMembers(room, callSession); const callMembers = useCallMembers(room, callSession);
@@ -77,7 +96,7 @@ function CallPrescreen() {
const callEmbed = useCallEmbed(); const callEmbed = useCallEmbed();
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId; const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
const canJoin = hasPermission && (livekitSupported || hasParticipant); const canJoin = hasPermission && livekitSupported && rtcSupported;
return ( return (
<Scroll variant="Surface" hideTrack> <Scroll variant="Surface" hideTrack>
@@ -100,7 +119,11 @@ function CallPrescreen() {
<Box className={css.PrescreenMessage} alignItems="Center"> <Box className={css.PrescreenMessage} alignItems="Center">
{!inOtherCall && {!inOtherCall &&
(hasPermission ? ( (hasPermission ? (
<JoinMessage hasParticipant={hasParticipant} livekitSupported={livekitSupported} /> <JoinMessage
hasParticipant={hasParticipant}
livekitSupported={livekitSupported}
rtcSupported={rtcSupported}
/>
) : ( ) : (
<NoPermissionMessage /> <NoPermissionMessage />
))} ))}
+19 -7
View File
@@ -23,12 +23,12 @@ import { useAtom, useAtomValue } from 'jotai';
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav'; import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge'; import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room'; import { getDirectRoomAvatarUrl, getRoomAvatarUrl, getStateEvent } from '../../utils/room';
import { nameInitials } from '../../utils/common'; import { nameInitials } from '../../utils/common';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomUnread } from '../../state/hooks/unread'; import { useRoomUnread } from '../../state/hooks/unread';
import { roomToUnreadAtom } from '../../state/room/roomToUnread'; import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { usePowerLevels } from '../../hooks/usePowerLevels'; import { getPowersLevelFromMatrixEvent, usePowerLevels } from '../../hooks/usePowerLevels';
import { copyToClipboard } from '../../utils/dom'; import { copyToClipboard } from '../../utils/dom';
import { markAsRead } from '../../utils/notifications'; import { markAsRead } from '../../utils/notifications';
import { UseStateProvider } from '../../components/UseStateProvider'; import { UseStateProvider } from '../../components/UseStateProvider';
@@ -49,8 +49,8 @@ import {
RoomNotificationMode, RoomNotificationMode,
} from '../../hooks/useRoomsNotificationPreferences'; } from '../../hooks/useRoomsNotificationPreferences';
import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher'; import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
import { useRoomCreators } from '../../hooks/useRoomCreators'; import { getRoomCreatorsForRoomId, useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions'; import { getRoomPermissionsAPI, useRoomPermissions } from '../../hooks/useRoomPermissions';
import { InviteUserPrompt } from '../../components/invite-user-prompt'; import { InviteUserPrompt } from '../../components/invite-user-prompt';
import { useRoomName } from '../../hooks/useRoomMeta'; import { useRoomName } from '../../hooks/useRoomMeta';
import { useCallMembers, useCallSession } from '../../hooks/useCall'; import { useCallMembers, useCallSession } from '../../hooks/useCall';
@@ -59,6 +59,8 @@ import { callChatAtom } from '../../state/callEmbed';
import { useCallPreferencesAtom } from '../../state/hooks/callPreferences'; import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
import { livekitSupport } from '../../hooks/useLivekitSupport'; import { livekitSupport } from '../../hooks/useLivekitSupport';
import { StateEvent } from '../../../types/matrix/room';
import { webRTCSupported } from '../../utils/rtc';
type RoomNavItemMenuProps = { type RoomNavItemMenuProps = {
room: Room; room: Room;
@@ -287,8 +289,18 @@ export function RoomNavItem({
const autoDiscoveryInfo = useAutoDiscoveryInfo(); const autoDiscoveryInfo = useAutoDiscoveryInfo();
const handleStartCall: MouseEventHandler<HTMLAnchorElement> = (evt) => { const handleStartCall: MouseEventHandler<HTMLAnchorElement> = (evt) => {
// Do not join if no livekit support or call is not started by others const powerLevelsEvent = getStateEvent(room, StateEvent.RoomPowerLevels);
if (!livekitSupport(autoDiscoveryInfo) && callMembers.length === 0) { const powerLevels = getPowersLevelFromMatrixEvent(powerLevelsEvent);
const creators = getRoomCreatorsForRoomId(mx, room.roomId);
const permissions = getRoomPermissionsAPI(creators, powerLevels);
const hasCallPermission = permissions.stateEvent(
StateEvent.GroupCallMemberPrefix,
mx.getSafeUserId()
);
// Do not join if missing permissions or no livekit support or no webRTC support
if (!hasCallPermission || !livekitSupport(autoDiscoveryInfo) || !webRTCSupported()) {
return; return;
} }
@@ -367,7 +379,7 @@ export function RoomNavItem({
aria-label={notificationMode} aria-label={notificationMode}
/> />
)} )}
{room.isCallRoom() && callMembers.length > 0 && ( {callMembers.length > 0 && (
<Badge variant="Critical" fill="Solid" size="400"> <Badge variant="Critical" fill="Solid" size="400">
<Text as="span" size="L400" truncate> <Text as="span" size="L400" truncate>
{callMembers.length} Live {callMembers.length} Live
@@ -23,7 +23,7 @@ export function Permissions({ requestClose }: PermissionsProps) {
const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId()); const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId());
const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId()); const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId());
const permissionGroups = usePermissionGroups(room.isCallRoom()); const permissionGroups = usePermissionGroups();
const [powerEditor, setPowerEditor] = useState(false); const [powerEditor, setPowerEditor] = useState(false);
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { MessageEvent, StateEvent } from '../../../../types/matrix/room'; import { MessageEvent, StateEvent } from '../../../../types/matrix/room';
import { PermissionGroup } from '../../common-settings/permissions'; import { PermissionGroup } from '../../common-settings/permissions';
export const usePermissionGroups = (isCallRoom: boolean): PermissionGroup[] => { export const usePermissionGroups = (): PermissionGroup[] => {
const groups: PermissionGroup[] = useMemo(() => { const groups: PermissionGroup[] = useMemo(() => {
const messagesGroup: PermissionGroup = { const messagesGroup: PermissionGroup = {
name: 'Messages', name: 'Messages',
@@ -54,7 +54,7 @@ export const usePermissionGroups = (isCallRoom: boolean): PermissionGroup[] => {
state: true, state: true,
key: StateEvent.GroupCallMemberPrefix, key: StateEvent.GroupCallMemberPrefix,
}, },
name: 'Join Call', name: 'Start or Join Call',
}, },
], ],
}; };
@@ -216,13 +216,13 @@ export const usePermissionGroups = (isCallRoom: boolean): PermissionGroup[] => {
return [ return [
messagesGroup, messagesGroup,
...(isCallRoom ? [callSettingsGroup] : []), callSettingsGroup,
moderationGroup, moderationGroup,
roomOverviewGroup, roomOverviewGroup,
roomSettingsGroup, roomSettingsGroup,
otherSettingsGroup, otherSettingsGroup,
]; ];
}, [isCallRoom]); }, []);
return groups; return groups;
}; };
+7 -1
View File
@@ -18,12 +18,18 @@ import { CallView } from '../call/CallView';
import { RoomViewHeader } from './RoomViewHeader'; import { RoomViewHeader } from './RoomViewHeader';
import { callChatAtom } from '../../state/callEmbed'; import { callChatAtom } from '../../state/callEmbed';
import { CallChatView } from './CallChatView'; import { CallChatView } from './CallChatView';
import { useCallEmbed } from '../../hooks/useCallEmbed';
import { useCallMembers, useCallSession } from '../../hooks/useCall';
export function Room() { export function Room() {
const { eventId } = useParams(); const { eventId } = useParams();
const room = useRoom(); const room = useRoom();
const mx = useMatrixClient(); const mx = useMatrixClient();
const callSession = useCallSession(room);
const callMembers = useCallMembers(room, callSession);
const callEmbed = useCallEmbed();
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer'); const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
@@ -43,7 +49,7 @@ export function Room() {
) )
); );
const callView = room.isCallRoom(); const callView = callEmbed?.roomId === room.roomId || room.isCallRoom() || callMembers.length > 0;
return ( return (
<PowerLevelsContextProvider value={powerLevels}> <PowerLevelsContextProvider value={powerLevels}>
+144 -1
View File
@@ -21,6 +21,7 @@ import {
RectCords, RectCords,
Badge, Badge,
Spinner, Spinner,
Button,
} from 'folds'; } from 'folds';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Room } from 'matrix-js-sdk'; import { Room } from 'matrix-js-sdk';
@@ -68,6 +69,9 @@ import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { InviteUserPrompt } from '../../components/invite-user-prompt'; import { InviteUserPrompt } from '../../components/invite-user-prompt';
import { ContainerColor } from '../../styles/ContainerColor.css'; import { ContainerColor } from '../../styles/ContainerColor.css';
import { RoomSettingsPage } from '../../state/roomSettings'; import { RoomSettingsPage } from '../../state/roomSettings';
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
import { webRTCSupported } from '../../utils/rtc';
type RoomMenuProps = { type RoomMenuProps = {
room: Room; room: Room;
@@ -253,6 +257,132 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
); );
}); });
type CallMenuProps = {
onVoiceCall: () => void;
onVideoCall: () => void;
requestClose: () => void;
};
const CallMenu = forwardRef<HTMLDivElement, CallMenuProps>(
({ requestClose, onVoiceCall, onVideoCall }, ref) => {
const handleVoice = () => {
onVoiceCall();
requestClose();
};
const handleVideo = () => {
onVideoCall();
requestClose();
};
return (
<Menu ref={ref} style={{ padding: config.space.S200, minWidth: toRem(150) }}>
<Box direction="Column" gap="200">
<Text size="L400">Start Call</Text>
<Box direction="Column" gap="200">
<Button
size="300"
variant="Success"
fill="Soft"
outlined
radii="300"
before={<Icon size="100" src={Icons.Phone} filled />}
onClick={handleVoice}
>
<Text size="B300">Voice</Text>
</Button>
<Button
size="300"
variant="Success"
radii="300"
before={<Icon size="100" src={Icons.VideoCamera} filled />}
onClick={handleVideo}
>
<Text size="B300">Video</Text>
</Button>
</Box>
</Box>
</Menu>
);
}
);
function CallButton() {
const room = useRoom();
const direct = useIsDirectRoom();
const callEmbed = useCallEmbed();
const startCall = useCallStart(direct);
const callStarted = callEmbed && callEmbed.roomId === room.roomId;
const inAnotherCall = callEmbed && !callStarted;
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
return (
<>
<TooltipProvider
position="Bottom"
offset={4}
tooltip={
<Tooltip>
{inAnotherCall ? (
<Text size="L400">Already in another call End the current call to join!</Text>
) : (
<Text>Call</Text>
)}
</Tooltip>
}
>
{(triggerRef) => (
<IconButton
variant="Surface"
fill="None"
ref={triggerRef}
onClick={handleOpenMenu}
onContextMenu={(evt) => {
evt.preventDefault();
startCall(room, {
microphone: true,
video: true,
sound: true,
});
}}
disabled={inAnotherCall || callStarted}
aria-pressed={!!menuAnchor}
>
<Icon size="400" src={Icons.VideoCamera} filled={!!menuAnchor} />
</IconButton>
)}
</TooltipProvider>
<PopOut
anchor={menuAnchor}
position="Bottom"
align="Center"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
returnFocusOnDeactivate: false,
onDeactivate: () => setMenuAnchor(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<CallMenu
onVideoCall={() => startCall(room, { microphone: true, video: true, sound: true })}
onVoiceCall={() => startCall(room, { microphone: true, video: false, sound: true })}
requestClose={() => setMenuAnchor(undefined)}
/>
</FocusTrap>
}
/>
</>
);
}
export function RoomViewHeader({ callView }: { callView?: boolean }) { export function RoomViewHeader({ callView }: { callView?: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const mx = useMatrixClient(); const mx = useMatrixClient();
@@ -260,6 +390,17 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const room = useRoom(); const room = useRoom();
const space = useSpaceOptionally(); const space = useSpaceOptionally();
const powerLevels = usePowerLevelsContext();
const creators = useRoomCreators(room);
const permissions = useRoomPermissions(creators, powerLevels);
const hasCallPermission = permissions.stateEvent(
StateEvent.GroupCallMemberPrefix,
mx.getSafeUserId()
);
const livekitSupported = useLivekitSupport();
const rtcSupported = webRTCSupported();
const [menuAnchor, setMenuAnchor] = useState<RectCords>(); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>(); const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>();
const direct = useIsDirectRoom(); const direct = useIsDirectRoom();
@@ -453,7 +594,9 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
</FocusTrap> </FocusTrap>
} }
/> />
{!room.isCallRoom() && livekitSupported && rtcSupported && hasCallPermission && (
<CallButton />
)}
{screenSize === ScreenSize.Desktop && ( {screenSize === ScreenSize.Desktop && (
<TooltipProvider <TooltipProvider
position="Bottom" position="Bottom"
+1 -1
View File
@@ -286,7 +286,7 @@ export function Search({ requestClose }: SearchProps) {
gap="100" gap="100"
> >
<Text size="H6" align="Center"> <Text size="H6" align="Center">
{result ? 'No Match Found' : `No Rooms'}`} {result ? 'No Match Found' : 'No Rooms'}
</Text> </Text>
<Text size="T200" align="Center"> <Text size="T200" align="Center">
{result {result
+1 -1
View File
@@ -46,7 +46,7 @@ export function About({ requestClose }: AboutProps) {
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
<Box gap="100" alignItems="End"> <Box gap="100" alignItems="End">
<Text size="H3">Cinny</Text> <Text size="H3">Cinny</Text>
<Text size="T200">v4.11.1</Text> <Text size="T200">v4.12.1</Text>
</Box> </Box>
<Text>Yet another matrix client.</Text> <Text>Yet another matrix client.</Text>
</Box> </Box>
+2 -1
View File
@@ -48,7 +48,7 @@ export const createCallEmbed = (
const ongoing = const ongoing =
MatrixRTCSession.sessionMembershipsForRoom(room, rtcSession.sessionDescription).length > 0; MatrixRTCSession.sessionMembershipsForRoom(room, rtcSession.sessionDescription).length > 0;
const intent = CallEmbed.getIntent(dm, ongoing); const intent = CallEmbed.getIntent(dm, ongoing, pref?.video);
const widget = CallEmbed.getWidget(mx, room, intent, themeKind); const widget = CallEmbed.getWidget(mx, room, intent, themeKind);
const controlState = pref && new CallControlState(pref.microphone, pref.video, pref.sound); const controlState = pref && new CallControlState(pref.microphone, pref.video, pref.sound);
@@ -101,6 +101,7 @@ export const useCallJoined = (embed?: CallEmbed): boolean => {
export const useCallHangupEvent = (embed: CallEmbed, callback: () => void) => { export const useCallHangupEvent = (embed: CallEmbed, callback: () => void) => {
useClientWidgetApiEvent(embed.call, ElementWidgetActions.HangupCall, callback); useClientWidgetApiEvent(embed.call, ElementWidgetActions.HangupCall, callback);
useClientWidgetApiEvent(embed.call, ElementWidgetActions.Close, callback);
}; };
export const useCallMemberSoundSync = (embed: CallEmbed) => { export const useCallMemberSoundSync = (embed: CallEmbed) => {
+4
View File
@@ -25,6 +25,10 @@ export const useDateFormatItems = (): DateFormatItem[] =>
format: 'YYYY/MM/DD', format: 'YYYY/MM/DD',
name: 'YYYY/MM/DD', name: 'YYYY/MM/DD',
}, },
{
format: 'YYYY-MM-DD',
name: 'YYYY-MM-DD',
},
{ {
format: '', format: '',
name: 'Custom', name: 'Custom',
+50 -6
View File
@@ -1,9 +1,10 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { MatrixClient, Method } from 'matrix-js-sdk';
import { useMatrixClient } from './useMatrixClient'; import { useMatrixClient } from './useMatrixClient';
import { AsyncState, useAsyncCallbackValue } from './useAsyncCallback'; import { AsyncState, useAsyncCallbackValue } from './useAsyncCallback';
import { useSpecVersions } from './useSpecVersions'; import { useSpecVersions } from './useSpecVersions';
export const useMutualRoomsSupport = (): boolean => { export const useUnstableMutualRoomsSupport = (): boolean => {
const { unstable_features: unstableFeatures } = useSpecVersions(); const { unstable_features: unstableFeatures } = useSpecVersions();
const supported = const supported =
@@ -14,16 +15,59 @@ export const useMutualRoomsSupport = (): boolean => {
return !!supported; return !!supported;
}; };
export const useMutualRoomsSupport = (): boolean => {
const { unstable_features: unstableFeatures, versions } = useSpecVersions();
const supported =
versions.includes('v1.19') ||
unstableFeatures?.['uk.half-shot.msc2666.query_mutual_rooms.stable'];
return !!supported;
};
type MutualRoomsOK = {
joined: string[];
next_batch?: string;
count: number;
};
const fetchAllMutualRooms = async (mx: MatrixClient, userId: string): Promise<string[]> => {
const mutualRooms: Set<string> = new Set();
let nextBatch: string | undefined;
do {
// eslint-disable-next-line no-await-in-loop
const result = await mx.http.authedRequest<MutualRoomsOK>(
Method.Get,
'/mutual_rooms',
{
user_id: userId,
from: nextBatch,
},
undefined,
{
prefix: '/_matrix/client/v1',
}
);
result.joined.forEach((r) => mutualRooms.add(r));
nextBatch = result.next_batch;
} while (typeof nextBatch === 'string');
return Array.from(mutualRooms);
};
export const useMutualRooms = (userId: string): AsyncState<string[], unknown> => { export const useMutualRooms = (userId: string): AsyncState<string[], unknown> => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const supported = useMutualRoomsSupport(); const unstableSupport = useUnstableMutualRoomsSupport();
const support = useMutualRoomsSupport();
const [mutualRoomsState] = useAsyncCallbackValue( const [mutualRoomsState] = useAsyncCallbackValue(
useCallback( useCallback(() => {
() => (supported ? mx._unstable_getSharedRooms(userId) : Promise.resolve([])), if (support) return fetchAllMutualRooms(mx, userId);
[mx, userId, supported] if (unstableSupport) return mx._unstable_getSharedRooms(userId);
) return Promise.resolve([]);
}, [mx, userId, unstableSupport, support])
); );
return mutualRoomsState; return mutualRoomsState;
+1 -1
View File
@@ -57,7 +57,7 @@ const fillMissingPowers = (powerLevels: IPowerLevels): IPowerLevels =>
return draftPl; return draftPl;
}); });
const getPowersLevelFromMatrixEvent = (mEvent?: MatrixEvent): IPowerLevels => { export const getPowersLevelFromMatrixEvent = (mEvent?: MatrixEvent): IPowerLevels => {
const plContent = mEvent?.getContent<IPowerLevels>(); const plContent = mEvent?.getContent<IPowerLevels>();
const powerLevels = !plContent ? DEFAULT_POWER_LEVELS : fillMissingPowers(plContent); const powerLevels = !plContent ? DEFAULT_POWER_LEVELS : fillMissingPowers(plContent);
+1 -1
View File
@@ -15,7 +15,7 @@ export function AuthFooter() {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
v4.11.1 v4.12.1
</Text> </Text>
<Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer"> <Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
Twitter Twitter
+1 -1
View File
@@ -22,7 +22,7 @@ export function SpecVersions({ baseUrl, children }: { baseUrl: string; children:
<Dialog> <Dialog>
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}> <Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
<Text> <Text>
Failed to connect to homeserver. Either homeserver is down or your internet. Unable to connect to the homeserver. The homeserver or your internet connection may be down.
</Text> </Text>
<Button variant="Critical" onClick={retry}> <Button variant="Critical" onClick={retry}>
<Text as="span" size="B400"> <Text as="span" size="B400">
+1 -1
View File
@@ -24,7 +24,7 @@ export function WelcomePage() {
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
v4.11.1 v4.12.1
</a> </a>
</span> </span>
} }
+33 -4
View File
@@ -47,12 +47,36 @@ export class CallEmbed {
private readonly disposables: Array<() => void> = []; private readonly disposables: Array<() => void> = [];
static getIntent(dm: boolean, ongoing: boolean): ElementCallIntent { static getIntent(dm: boolean, ongoing: boolean, video?: boolean): ElementCallIntent {
if (ongoing) { if (dm && ongoing) {
return dm ? ElementCallIntent.JoinExistingDM : ElementCallIntent.JoinExisting; return video ? ElementCallIntent.JoinExistingDM : ElementCallIntent.JoinExistingDMVoice;
}
if (dm) {
return video ? ElementCallIntent.StartCallDM : ElementCallIntent.StartCallDMVoice;
} }
return dm ? ElementCallIntent.StartCallDM : ElementCallIntent.StartCall; if (ongoing) {
return video ? ElementCallIntent.JoinExisting : ElementCallIntent.JoinExistingVoice;
}
return video ? ElementCallIntent.StartCall : ElementCallIntent.StartCallVoice;
}
static dmCall(intent: ElementCallIntent): boolean {
return (
intent === ElementCallIntent.JoinExistingDM ||
intent === ElementCallIntent.JoinExistingDMVoice ||
intent === ElementCallIntent.StartCallDM ||
intent === ElementCallIntent.StartCallDMVoice
);
}
static startingCall(intent: ElementCallIntent): boolean {
return (
intent === ElementCallIntent.StartCallDM ||
intent === ElementCallIntent.StartCallDMVoice ||
intent === ElementCallIntent.StartCall ||
intent === ElementCallIntent.StartCallVoice
);
} }
static getWidget( static getWidget(
@@ -81,8 +105,13 @@ export class CallEmbed {
perParticipantE2EE: room.hasEncryptionStateEvent().toString(), perParticipantE2EE: room.hasEncryptionStateEvent().toString(),
lang: 'en-EN', lang: 'en-EN',
theme: themeKind, theme: themeKind,
header: 'none',
}); });
if (!room.isCallRoom() && CallEmbed.startingCall(intent)) {
params.append('sendNotificationType', CallEmbed.dmCall(intent) ? 'ring' : 'notification');
}
const widgetUrl = new URL( const widgetUrl = new URL(
`${trimTrailingSlash(import.meta.env.BASE_URL)}/public/element-call/index.html`, `${trimTrailingSlash(import.meta.env.BASE_URL)}/public/element-call/index.html`,
window.location.origin window.location.origin
+2
View File
@@ -1,6 +1,8 @@
export enum ElementCallIntent { export enum ElementCallIntent {
StartCall = 'start_call', StartCall = 'start_call',
JoinExisting = 'join_existing', JoinExisting = 'join_existing',
StartCallVoice = 'start_call_voice',
JoinExistingVoice = 'join_existing_voice',
StartCallDM = 'start_call_dm', StartCallDM = 'start_call_dm',
JoinExistingDM = 'join_existing_dm', JoinExistingDM = 'join_existing_dm',
StartCallDMVoice = 'start_call_dm_voice', StartCallDMVoice = 'start_call_dm_voice',
+3 -7
View File
@@ -15,6 +15,8 @@ export function getCallCapabilities(
capabilities.add(MatrixCapabilities.Screenshots); capabilities.add(MatrixCapabilities.Screenshots);
capabilities.add(MatrixCapabilities.AlwaysOnScreen); capabilities.add(MatrixCapabilities.AlwaysOnScreen);
capabilities.add(MatrixCapabilities.MSC4039UploadFile);
capabilities.add(MatrixCapabilities.MSC4039DownloadFile);
capabilities.add(MatrixCapabilities.MSC3846TurnServers); capabilities.add(MatrixCapabilities.MSC3846TurnServers);
capabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent); capabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent);
capabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent); capabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent);
@@ -78,19 +80,13 @@ export function getCallCapabilities(
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomCreate).raw WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomCreate).raw
); );
capabilities.add(
WidgetEventCapability.forRoomEvent(
EventDirection.Receive,
'org.matrix.msc4075.rtc.notification'
).raw
);
[ [
'io.element.call.encryption_keys', 'io.element.call.encryption_keys',
'org.matrix.rageshake_request', 'org.matrix.rageshake_request',
EventType.Reaction, EventType.Reaction,
EventType.RoomRedaction, EventType.RoomRedaction,
'io.element.call.reaction', 'io.element.call.reaction',
'org.matrix.msc4075.rtc.notification',
'org.matrix.msc4310.rtc.decline', 'org.matrix.msc4310.rtc.decline',
].forEach((type) => { ].forEach((type) => {
capabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Send, type).raw); capabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Send, type).raw);
+2 -10
View File
@@ -1,12 +1,5 @@
import { replaceMatch } from '../internal'; import { replaceMatch } from '../internal';
import { import { BlockQuoteRule, CodeBlockRule, ESC_BLOCK_SEQ, HeadingRule, ListRule } from './rules';
BlockQuoteRule,
CodeBlockRule,
ESC_BLOCK_SEQ,
HeadingRule,
OrderedListRule,
UnorderedListRule,
} from './rules';
import { runBlockRule } from './runner'; import { runBlockRule } from './runner';
import { BlockMDParser } from './type'; import { BlockMDParser } from './type';
@@ -23,8 +16,7 @@ export const parseBlockMD: BlockMDParser = (text, parseInline) => {
if (!result) result = runBlockRule(text, CodeBlockRule, parseBlockMD, parseInline); if (!result) result = runBlockRule(text, CodeBlockRule, parseBlockMD, parseInline);
if (!result) result = runBlockRule(text, BlockQuoteRule, parseBlockMD, parseInline); if (!result) result = runBlockRule(text, BlockQuoteRule, parseBlockMD, parseInline);
if (!result) result = runBlockRule(text, OrderedListRule, parseBlockMD, parseInline); if (!result) result = runBlockRule(text, ListRule, parseBlockMD, parseInline);
if (!result) result = runBlockRule(text, UnorderedListRule, parseBlockMD, parseInline);
if (!result) result = runBlockRule(text, HeadingRule, parseBlockMD, parseInline); if (!result) result = runBlockRule(text, HeadingRule, parseBlockMD, parseInline);
// replace \n with <br/> because want to preserve empty lines // replace \n with <br/> because want to preserve empty lines
+148 -49
View File
@@ -10,14 +10,22 @@ export const HeadingRule: BlockMDRule = {
}, },
}; };
const CODEBLOCK_MD_1 = '```'; // opening fence: 3 or more backticks
const CODEBLOCK_REG_1 = /^`{3}(\S*)\n((?:.*\n)+?)`{3} *(?!.)\n?/m; // capture the exact fence length in group 1
// optional info string in group 2
// code content in group 3
// closing fence must match the exact same fence sequence via \1
const CODEBLOCK_REG_1 = /^(`{3,})(?!`)(\S*)\n((?:.*\n)+?)\1 *(?!.)\n?/m;
export const CodeBlockRule: BlockMDRule = { export const CodeBlockRule: BlockMDRule = {
match: (text) => text.match(CODEBLOCK_REG_1), match: (text) => text.match(CODEBLOCK_REG_1),
html: (match) => { html: (match) => {
const [, g1, g2] = match; const [, fence, g1, g2] = match;
const classNameAtt = g1 ? ` class="language-${g1}"` : ''; // use last identifier after dot, e.g. for "example.json" gets us "json" as language code.
return `<pre data-md="${CODEBLOCK_MD_1}"><code${classNameAtt}>${g2}</code></pre>`; const langCode = g1 ? g1.substring(g1.lastIndexOf('.') + 1) : null;
const filename = g1 !== langCode ? g1 : null;
const classNameAtt = langCode ? ` class="language-${langCode}"` : '';
const filenameAtt = filename ? ` data-label="${filename}"` : '';
return `<pre data-md="${fence}"><code${classNameAtt}${filenameAtt}>${g2}</code></pre>`;
}, },
}; };
@@ -44,55 +52,146 @@ export const BlockQuoteRule: BlockMDRule = {
}; };
const ORDERED_LIST_MD_1 = '-'; const ORDERED_LIST_MD_1 = '-';
const O_LIST_ITEM_PREFIX = /^(-|[\da-zA-Z]\.) */;
const O_LIST_START = /^([\d])\./;
const O_LIST_TYPE = /^([aAiI])\./;
const O_LIST_TRAILING_NEWLINE = /\n$/;
const ORDERED_LIST_REG_1 = /(^(?:-|[\da-zA-Z]\.) +.+\n?)+/m;
export const OrderedListRule: BlockMDRule = {
match: (text) => text.match(ORDERED_LIST_REG_1),
html: (match, parseInline) => {
const [listText] = match;
const [, listStart] = listText.match(O_LIST_START) ?? [];
const [, listType] = listText.match(O_LIST_TYPE) ?? [];
const lines = listText
.replace(O_LIST_TRAILING_NEWLINE, '')
.split('\n')
.map((lineText) => {
const line = lineText.replace(O_LIST_ITEM_PREFIX, '');
const txt = parseInline ? parseInline(line) : line;
return `<li><p>${txt}</p></li>`;
})
.join('');
const dataMdAtt = `data-md="${listType || listStart || ORDERED_LIST_MD_1}"`;
const startAtt = listStart ? ` start="${listStart}"` : '';
const typeAtt = listType ? ` type="${listType}"` : '';
return `<ol ${dataMdAtt}${startAtt}${typeAtt}>${lines}</ol>`;
},
};
const UNORDERED_LIST_MD_1 = '*'; const UNORDERED_LIST_MD_1 = '*';
const U_LIST_ITEM_PREFIX = /^\* */; const LIST_ITEM_REG = /^( *)([-*]|[\da-zA-Z]\.) +(.+)$/;
const U_LIST_TRAILING_NEWLINE = /\n$/; type ListType = 'ol' | 'ul';
const UNORDERED_LIST_REG_1 = /(^\* +.+\n?)+/m;
export const UnorderedListRule: BlockMDRule = { function getListType(marker: string): ListType {
match: (text) => text.match(UNORDERED_LIST_REG_1), return marker === '*' ? 'ul' : 'ol';
}
function getOrderedMeta(marker: string) {
const startMatch = marker.match(/^(\d)\./);
const typeMatch = marker.match(/^([aAiI])\./);
return {
start: startMatch?.[1],
type: typeMatch?.[1],
};
}
interface ParsedLine {
indent: number;
marker: string;
content: string;
listType: ListType;
}
function parseLines(text: string): ParsedLine[] {
return text
.replace(/\n$/, '')
.split('\n')
.map((line) => {
const match = line.match(LIST_ITEM_REG);
if (!match) return null;
const [, spaces, marker, content] = match;
return {
indent: spaces.length,
marker,
content,
listType: getListType(marker),
};
})
.filter(Boolean) as ParsedLine[];
}
function openList(line: ParsedLine) {
if (line.listType === 'ul') {
return `<ul data-md="${UNORDERED_LIST_MD_1}">`;
}
const { type, start } = getOrderedMeta(line.marker);
const dataMdAtt = `data-md="${type || start || ORDERED_LIST_MD_1}"`;
const startAtt = start ? ` start="${start}"` : '';
const typeAtt = type ? ` type="${type}"` : '';
return `<ol ${dataMdAtt}${startAtt}${typeAtt}>`;
}
function closeList(listType: ListType) {
return listType === 'ul' ? '</ul>' : '</ol>';
}
function buildList(lines: ParsedLine[], parseInline?: (s: string) => string): string {
let html = '';
const stack: ('ul' | 'ol')[] = [];
lines.forEach((line, index) => {
const prev = lines[index - 1];
const next = lines[index + 1];
const content = parseInline ? parseInline(line.content) : line.content;
// FIRST ITEM
if (!prev) {
html += openList(line);
stack.push(line.listType);
}
// DEEPER INDENT > open nested list
else if (line.indent > prev.indent) {
html += openList(line);
stack.push(line.listType);
}
// SAME LEVEL
else if (line.indent === prev.indent) {
html += '</li>';
// different list type
if (line.listType !== prev.listType) {
html += closeList(stack.pop()!);
html += openList(line);
stack.push(line.listType);
}
}
// GOING BACK UP
else if (line.indent < prev.indent) {
html += '</li>';
while (stack.length > line.indent + 1) {
html += closeList(stack.pop()!);
html += '</li>';
}
if (line.listType !== stack[stack.length - 1]) {
html += closeList(stack.pop()!);
html += openList(line);
stack.push(line.listType);
}
}
html += `<li><p>${content}</p>`;
// LAST ITEM cleanup
if (!next) {
html += '</li>';
while (stack.length) {
html += closeList(stack.pop()!);
}
}
});
return html;
}
const LIST_REG_1 = /^(?: *(?:[-*]|[\da-zA-Z]\.) +.+\n?)+/m;
export const ListRule: BlockMDRule = {
match: (text) => text.match(LIST_REG_1),
html: (match, parseInline) => { html: (match, parseInline) => {
const [listText] = match; const [listText] = match;
const lines = listText const lines = parseLines(listText);
.replace(U_LIST_TRAILING_NEWLINE, '')
.split('\n')
.map((lineText) => {
const line = lineText.replace(U_LIST_ITEM_PREFIX, '');
const txt = parseInline ? parseInline(line) : line;
return `<li><p>${txt}</p></li>`;
})
.join('');
return `<ul data-md="${UNORDERED_LIST_MD_1}">${lines}</ul>`; const html = buildList(lines, parseInline);
return html;
}, },
}; };
+4 -3
View File
@@ -232,8 +232,9 @@ export function CodeBlock({
opts: HTMLReactParserOptions; opts: HTMLReactParserOptions;
}) { }) {
const code = children[0]; const code = children[0];
const languageClass = const attribs = code instanceof Element && code.name === 'code' ? code.attribs : undefined;
code instanceof Element && code.name === 'code' ? code.attribs.class : undefined; const languageClass = attribs?.class;
const customLabel = attribs?.['data-label'];
const language = const language =
languageClass && languageClass.startsWith('language-') languageClass && languageClass.startsWith('language-')
? languageClass.replace('language-', '') ? languageClass.replace('language-', '')
@@ -262,7 +263,7 @@ export function CodeBlock({
<Header variant="Surface" size="400" className={css.CodeBlockHeader}> <Header variant="Surface" size="400" className={css.CodeBlockHeader}>
<Box grow="Yes"> <Box grow="Yes">
<Text size="L400" truncate> <Text size="L400" truncate>
{language ?? 'Code'} {customLabel ?? language ?? 'Code'}
</Text> </Text>
</Box> </Box>
<Box shrink="No" gap="200"> <Box shrink="No" gap="200">
+7 -1
View File
@@ -1,7 +1,13 @@
import { atom } from 'jotai'; import { atom } from 'jotai';
const STORAGE_KEY = 'settings'; const STORAGE_KEY = 'settings';
export type DateFormat = 'D MMM YYYY' | 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD' | ''; export type DateFormat =
| 'D MMM YYYY'
| 'DD/MM/YYYY'
| 'MM/DD/YYYY'
| 'YYYY/MM/DD'
| 'YYYY-MM-DD'
| '';
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500'; export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
export enum MessageLayout { export enum MessageLayout {
Modern = 0, Modern = 0,
+47
View File
@@ -0,0 +1,47 @@
import { keyframes, style } from '@vanilla-extract/css';
import { color, toRem } from 'folds';
const wobble = keyframes({
'0%': {
transform: 'translateX(0) rotateZ(0deg)',
},
'20%': {
transform: `translateX(-${toRem(4)}) rotateZ(-4deg)`,
},
'40%': {
transform: `translateX(${toRem(4)}) rotateZ(4deg)`,
},
'60%': {
transform: `translateX(-${toRem(3)}) rotateZ(-3deg)`,
},
'80%': {
transform: `translateX(${toRem(3)}) rotateZ(3deg)`,
},
'100%': {
transform: 'translateX(0) rotateZ(0deg)',
},
});
const glowPulse = keyframes({
'0%': {
boxShadow: `0 0 0 ${toRem(0)} ${color.Success.ContainerActive}`,
},
'100%': {
boxShadow: `0 0 0 ${toRem(8)} ${color.Success.ContainerActive}`,
},
});
export const WobbleAnimation = style({
animation: `${wobble} 2000ms ease-in-out`,
animationIterationCount: 'infinite',
});
export const GlowAnimation = style({
animation: `${glowPulse} 2000ms ease-out`,
animationIterationCount: 'infinite',
});
export const CallAvatarAnimation = style({
animation: `${wobble} 2000ms ease-in-out, ${glowPulse} 2000ms ease-out`,
animationIterationCount: 'infinite',
});
+11
View File
@@ -120,12 +120,23 @@ export const CodeBlockBottomShadow = style({
background: `linear-gradient(to top, #00000022, #00000000)`, background: `linear-gradient(to top, #00000022, #00000000)`,
}); });
const BaseList = style({});
export const List = style([ export const List = style([
BaseList,
DefaultReset, DefaultReset,
MarginSpaced, MarginSpaced,
{ {
padding: `0 ${config.space.S100}`, padding: `0 ${config.space.S100}`,
paddingLeft: config.space.S600, paddingLeft: config.space.S600,
selectors: {
'& &': {
marginTop: config.space.S200,
marginBottom: config.space.S200,
},
'li:last-child &': {
marginBottom: 0,
},
},
}, },
]); ]);
+9 -1
View File
@@ -233,7 +233,15 @@ export const notificationPermission = (permission: NotificationPermission) => {
if ('Notification' in window) { if ('Notification' in window) {
return window.Notification.permission === permission; return window.Notification.permission === permission;
} }
return false; try {
// https://stackoverflow.com/questions/29774836/failed-to-construct-notification-illegal-constructor
// https://issues.chromium.org/issues/40415865
// eslint-disable-next-line no-new
new Notification('');
} catch {
return false;
}
return true;
}; };
export const getMouseEventCords = (event: MouseEvent) => ({ export const getMouseEventCords = (event: MouseEvent) => ({
+5
View File
@@ -31,6 +31,7 @@ export const APPLICATION_MIME_TYPES = [
'application/javascript', 'application/javascript',
'application/xhtml+xml', 'application/xhtml+xml',
'application/xml', 'application/xml',
'application/ogg',
]; ];
export const TEXT_MIME_TYPE = [ export const TEXT_MIME_TYPE = [
@@ -115,6 +116,10 @@ export const getBlobSafeMimeType = (mimeType: string) => {
if (type === 'video/quicktime') { if (type === 'video/quicktime') {
return 'video/mp4'; return 'video/mp4';
} }
// Fixes missing playback for Ogg audio
if (type === 'application/ogg') {
return 'audio/ogg';
}
return type; return type;
}; };
+4
View File
@@ -0,0 +1,4 @@
export const webRTCSupported = () =>
['RTCPeerConnection', 'webkitRTCPeerConnection', 'mozRTCPeerConnection', 'RTCIceGatherer'].some(
(item) => item in window
);
+1 -1
View File
@@ -71,7 +71,7 @@ const permittedTagToAttributes = {
ul: ['data-md'], ul: ['data-md'],
a: ['name', 'target', 'href', 'rel', 'data-md'], a: ['name', 'target', 'href', 'rel', 'data-md'],
img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'], img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'],
code: ['class', 'data-md'], code: ['class', 'data-md', 'data-label'],
strong: ['data-md'], strong: ['data-md'],
i: ['data-md'], i: ['data-md'],
em: ['data-md'], em: ['data-md'],
+6
View File
@@ -129,3 +129,9 @@ textarea {
audio:not([controls]) { audio:not([controls]) {
display: none !important; display: none !important;
} }
/* Fix Firefox rendering lists that have empty items with those items collapsed in on eachother */
li p::before {
content: '';
display: inline-block;
}