Compare commits

..

1 Commits

Author SHA1 Message Date
Ajay Bura a7f2d1684f improve thread reply layout 2025-08-04 20:31:38 +05:30
1003 changed files with 19800 additions and 69933 deletions
-15
View File
@@ -1,15 +0,0 @@
{
"defaultHomeserver": 0,
"homeserverList": ["matrix.lotusguild.org"],
"allowCustomHomeservers": false,
"featuredCommunities": {
"openAsDefault": false,
"spaces": [],
"rooms": [],
"servers": []
},
"hashRouter": {
"enabled": false,
"basename": "/"
}
}
-17
View File
@@ -1,17 +0,0 @@
{
"defaultHomeserver": 0,
"homeserverList": [
"matrix.lotusguild.org"
],
"allowCustomHomeservers": false,
"featuredCommunities": {
"openAsDefault": false,
"spaces": [],
"rooms": [],
"servers": []
},
"hashRouter": {
"enabled": false,
"basename": "/"
}
}
-1
View File
@@ -1 +0,0 @@
VITE_APP_VERSION=lotus
+2
View File
@@ -0,0 +1,2 @@
experiment
node_modules
+72
View File
@@ -0,0 +1,72 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
'airbnb',
'prettier',
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
"globals": {
JSX: "readonly"
},
plugins: [
'react',
'@typescript-eslint'
],
rules: {
'linebreak-style': 0,
'no-underscore-dangle': 0,
"no-shadow": "off",
"import/prefer-default-export": "off",
"import/extensions": "off",
"import/no-unresolved": "off",
"import/no-extraneous-dependencies": [
"error",
{
devDependencies: true,
},
],
'react/no-unstable-nested-components': [
'error',
{ allowAsProps: true },
],
"react/jsx-filename-extension": [
"error",
{
extensions: [".tsx", ".jsx"],
},
],
"react/require-default-props": "off",
"react/jsx-props-no-spreading": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-shadow": "error"
},
overrides: [
{
files: ['*.ts'],
rules: {
'no-undef': 'off',
},
},
],
};
-118
View File
@@ -1,118 +0,0 @@
name: CI
on:
push:
branches: [lotus]
pull_request:
branches: [lotus]
jobs:
build:
name: Build & Quality Checks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
cache: npm
- name: Install dependencies
# Harden against transient registry network failures (ECONNRESET etc.):
# raise npm's built-in fetch retries/timeouts and retry `npm ci` up to
# 3 times with backoff before failing the build.
run: |
npm config set fetch-retries 5
npm config set fetch-retry-mintimeout 20000
npm config set fetch-retry-maxtimeout 120000
npm config set fetch-timeout 600000
for attempt in 1 2 3; do
echo "npm ci attempt $attempt…"
npm ci && break
if [ "$attempt" = "3" ]; then
echo "npm ci failed after 3 attempts" >&2
exit 1
fi
echo "npm ci failed; retrying in $((attempt * 15))s…" >&2
sleep $((attempt * 15))
done
# ── Critical gate — if this fails, nothing deploys ──────────────────
- name: Build
run: npm run build
env:
NODE_OPTIONS: '--max_old_space_size=4096'
VITE_APP_VERSION: ${{ github.sha }}
# Unit tests are a hard gate too — deterministic pure-logic tests on Node's
# built-in runner via tsx (no vitest — Vite 8 is ahead of vitest's range).
# A failure blocks the deploy.
- name: Unit tests
run: npm test
# ── Quality checks (informational — pre-existing issues exist) ───────
- name: TypeScript
run: npm run typecheck
continue-on-error: true
- name: ESLint
run: npm run check:eslint
continue-on-error: true
- name: Prettier
run: npm run check:prettier
continue-on-error: true
# ── Security ─────────────────────────────────────────────────────────
- name: Audit (high/critical)
run: npm audit --audit-level=high --omit=dev
continue-on-error: true
# ── Bundle size report ───────────────────────────────────────────────
- name: Report bundle sizes
run: |
echo "### Bundle sizes" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| File | Size | Gzip |" >> $GITHUB_STEP_SUMMARY
echo "|------|------|------|" >> $GITHUB_STEP_SUMMARY
find dist/assets -name "*.js" -not -name "*.map" | sort | while read f; do
name=$(basename "$f")
size=$(du -sh "$f" | cut -f1)
gzip_size=$(gzip -c "$f" | wc -c | awk '{printf "%.1f kB", $1/1024}')
echo "| $name | $size | $gzip_size |" >> $GITHUB_STEP_SUMMARY
done
# ── Desktop build trigger ──────────────────────────────────────────────
# Gated on `build` succeeding so a broken push (e.g. failing `npm ci` or
# `npm run build`) never bumps the cinny-desktop submodule and kicks off the
# slow Tauri release builds, which would only error out downstream. Only
# runs on a real push to lotus — not on pull_request CI runs.
trigger-desktop:
name: Trigger Desktop Build
needs: build
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/lotus' }}
runs-on: ubuntu-latest
steps:
- name: Bump cinny submodule
env:
TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
CINNY_SHA="${{ github.sha }}"
git clone "https://x-access-token:$TOKEN@code.lotusguild.org/LotusGuild/cinny-desktop.git" desktop
cd desktop
git config user.email "ci@lotusguild.org"
git config user.name "Lotus CI"
git submodule update --init cinny
git -C cinny fetch origin
git -C cinny checkout "$CINNY_SHA"
git add cinny
if git diff --cached --quiet; then
echo "Submodule already at $CINNY_SHA, nothing to do"
else
git commit -m "chore: bump cinny submodule to ${CINNY_SHA:0:8}"
git push origin main
echo "Pushed — cinny-desktop release.yml will start via on:push trigger"
fi
@@ -1,127 +0,0 @@
labels: ['needs-confirmation']
body:
- type: markdown #add faqs in future
attributes:
value: |
> [!IMPORTANT]
> Please read through [the Discussion rules](https://github.com/cinnyapp/cinny/discussions/2653) and check for both existing [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
- type: markdown
attributes:
value: '# Issue Details'
- type: textarea
attributes:
label: Issue Description
description: |
Provide a detailed description of the issue. Include relevant information, such as:
- The feature or configuration option you encounter the issue with.
- Screenshots, screen recordings, or other supporting media (as needed).
- If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description.
placeholder: |
When I try to send a message in a room, the message doesn't appear in the timeline.
OR
The application crashes when I click on the settings button.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: |
Describe how you expect Cinny to behave in this situation.
placeholder: |
I expected the message to appear in the room timeline immediately after sending.
OR
The settings panel should open smoothly without any crashes.
validations:
required: true
- type: textarea
attributes:
label: Actual Behavior
description: |
Describe how Cinny actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically.
placeholder: |
The application freezes for 3 seconds and then shows a white screen.
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
description: |
Provide a detailed set of step-by-step instructions for reproducing this issue.
placeholder: |
1. Open Cinny and log in to my account
2. Navigate to the #general room
3. Type a message in the message box
4. Press Enter to send
5. Notice that the message doesn't appear in the timeline
validations:
required: true
- type: textarea
attributes:
label: Environement
description: |
Please provide information about your environment. Include the following:
- OS:
- Browser:
- Cinny Web Version: (app.cinny.in or self hosted)
- Cinny desktop Version: (appimage or deb or flatpak)
- Matrix Homeserver:
placeholder: |
- OS: Windows 11
- Browser: Chrome 120.0.6099.109
- Cinny Web Version: 3.2.0 (app.cinny.in or self hosted)
- Cinny desktop Version: 3.2.0 (appimage or deb or flatpak)
- Matrix Homeserver: matrix.org (Synapse 1.97.0)
render: text
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant Logs
description: |
If applicable, add browser console logs to help explain your problem.
**To get browser console logs:**
- Chrome/Edge: Press F12 → Console tab
- Firefox: Press F12 → Console tab
- Safari: Develop → Show Web Inspector → Console
Please wrap large log outputs in code blocks with triple backticks (```).
placeholder: |
```
Error: Failed to send message
at MessageComposer.sendMessage (composer.js:245)
at HTMLButtonElement.onClick (composer.js:189)
TypeError: Cannot read property 'content' of undefined
at RoomTimeline.render (timeline.js:567)
```
render: shell
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: |
Add any other context about the problem here (e.g., when did this start happening, does it happen on different homeservers, etc.)
placeholder: |
- This started happening after I updated to version 3.2.0
- It only happens in encrypted rooms, not in public rooms
- I've tried on both Firefox and Chrome with the same result
- It works fine on my phone using the same account
- This happens on all homeservers I've tested (matrix.org, mozilla.org)
validations:
required: false
- type: markdown
attributes:
value: |
# User Acknowledgements
> [!TIP]
> Use these links to review the existing Cinny [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc).
- type: checkboxes #add faqs in future
attributes:
label: 'I acknowledge that:'
options:
- label: I have searched the Cinny repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
required: true
- label: I have checked the "Preview" tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (` ``` `) on separate lines.
required: true
+57
View File
@@ -0,0 +1,57 @@
name: 🐞 Bug Report
description: Report a bug
body:
- type: markdown
attributes:
value: |
## First of all
1. Please search for [existing issues](https://github.com/ajbura/cinny/issues?q=is%3Aissue) about this problem first.
2. Make sure Cinny is up to date.
3. Make sure it's an issue with Cinny and not something else you are using.
4. Remember to be friendly.
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear description of what the bug is. Include screenshots if applicable.
placeholder: Bug description
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Steps to reproduce the behavior.
placeholder: |
1. Go to ...
2. Click on ...
3. See error
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear description of what you expected to happen.
- type: textarea
id: info
attributes:
label: Platform and versions
description: "Provide OS, browser and Cinny version with your Homeserver."
placeholder: |
1. OS: [e.g. Windows 10, MacOS]
2. Browser: [e.g. chrome 99.5, firefox 97.2]
3. Cinny version: [e.g. 1.8.1 (app.cinny.in)]
4. Matrix homeserver: [e.g. matrix.org]
render: shell
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
+3 -4
View File
@@ -1,5 +1,4 @@
blank_issues_enabled: false
contact_links:
- name: Features, Bug Reports, Questions
url: https://github.com/cinnyapp/cinny/discussions/new/choose
about: Our preferred starting point if you have any questions or suggestions about features or behavior.
- name: 💬 Matrix Chat
url: https://matrix.to/#/#cinny:matrix.org
about: Ask questions and talk to other Cinny users and the maintainers
@@ -0,0 +1,33 @@
name: 💡 Feature Request
description: Suggest an idea
body:
- type: textarea
id: problem
attributes:
label: Describe the problem
description: A clear description of the problem this feature would solve
placeholder: "I'm always frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like"
description: A clear description of what change you would like
placeholder: "I would like to..."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: "Any alternative solutions you've considered"
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
-9
View File
@@ -1,9 +0,0 @@
---
name: Pre-Discussed and Approved Topics
about: |-
Only for topics already discussed and approved in the GitHub Discussions section.
---
**DO NOT OPEN A NEW ISSUE. PLEASE USE THE DISCUSSIONS SECTION.**
**I DIDN'T READ THE ABOVE LINE. PLEASE CLOSE THIS ISSUE.**
+22
View File
@@ -0,0 +1,22 @@
<!-- Please read https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
### Description
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes #
#### Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
### Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
+3
View File
@@ -0,0 +1,3 @@
# Reporting a Vulnerability
**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**
+14 -14
View File
@@ -2,29 +2,29 @@
version: 2
updates:
# - package-ecosystem: npm
# directory: /
# schedule:
# interval: weekly
# day: "tuesday"
# time: "01:00"
# timezone: "Asia/Kolkata"
# open-pull-requests-limit: 15
# - package-ecosystem: npm
# directory: /
# schedule:
# interval: weekly
# day: "tuesday"
# time: "01:00"
# timezone: "Asia/Kolkata"
# open-pull-requests-limit: 15
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: 'tuesday'
time: '01:00'
timezone: 'Asia/Kolkata'
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 5
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
day: 'tuesday'
time: '01:00'
timezone: 'Asia/Kolkata'
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 5
+1 -13
View File
@@ -1,22 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":dependencyDashboardApproval",
":semanticCommits",
"group:monorepos"
],
"extends": ["config:recommended", ":dependencyDashboardApproval"],
"labels": ["Dependencies"],
"rebaseWhen": "conflicted",
"packageRules": [
{
"matchUpdateTypes": ["lockFileMaintenance"]
},
{
"matchPackageNames": ["slate", "slate-dom", "slate-history", "slate-react"]
},
{
"matchPackageNames": ["linkifyjs", "linkify-react"]
}
],
"lockFileMaintenance": {
+6 -6
View File
@@ -12,12 +12,12 @@ jobs:
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4.2.0
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'
package-manager-cache: false
node-version: 20.12.2
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build app
@@ -25,7 +25,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@v4.6.2
with:
name: preview
path: dist
@@ -33,7 +33,7 @@ jobs:
- name: Save pr number
run: echo ${PR_NUMBER} > ./pr.txt
- name: Upload pr number
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@v4.6.2
with:
name: pr
path: ./pr.txt
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
- name: 'CLA Assistant'
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: cla-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
uses: cla-assistant/github-action@v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret
+15 -22
View File
@@ -1,10 +1,9 @@
name: Deploy PR to Netlify
run-name: 'Deploy PR to Netlify (${{ github.event.workflow_run.head_branch }})'
on:
workflow_run:
workflows: ['Build pull request']
types: [completed]
workflows: ["Build pull request"]
types: [completed]
jobs:
deploy-pull-request:
@@ -16,22 +15,16 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download pr number
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
with:
workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }}
name: pr
- name: Validate and output pr number
- name: Output pr number
id: pr
run: |
PR_ID=$(<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}"
run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT
- name: Download artifact
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
with:
workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }}
@@ -39,25 +32,25 @@ jobs:
path: dist
- name: Deploy to Netlify
id: netlify
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
with:
publish-dir: dist
deploy-message: 'Deploy PR ${{ steps.pr.outputs.id }}'
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
alias: ${{ steps.pr.outputs.id }}
# These don't work because we're in workflow_run
enable-pull-request-comment: false
enable-commit-comment: false
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN_PR }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PR_CINNY }}
timeout-minutes: 1
- name: Comment preview on PR
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b #v3.0.1
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6
env:
github-token: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
pr-number: ${{ steps.pr.outputs.id }}
comment-tag: ${{ steps.pr.outputs.id }}
pr_number: ${{ steps.pr.outputs.id }}
comment_tag: ${{ steps.pr.outputs.id }}
message: |
Preview: ${{ steps.netlify.outputs.deploy-url }}
⚠️ Exercise caution. Use test accounts. ⚠️
Preview: ${{ steps.netlify.outputs.deploy-url }}
⚠️ Exercise caution. Use test accounts. ⚠️
+3 -47
View File
@@ -5,59 +5,15 @@ on:
paths:
- 'Dockerfile'
- '.github/workflows/docker-pr.yml'
- '.github/workflows/prod-deploy.yml'
jobs:
docker-build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Login to Docker Hub #Do not update this action from a outside PR
if: github.event.pull_request.head.repo.fork == false
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
continue-on-error: true
- name: Login to the Github Container registry #Do not update this action from a outside PR
if: github.event.pull_request.head.repo.fork == false
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Extract metadata (tags, labels) for Docker, GHCR
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: |
ajbura/cinny
ghcr.io/${{ github.repository }}
- name: Build Docker image (no push)
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: actions/checkout@v4.2.0
- name: Build Docker image
uses: docker/build-push-action@v6.18.0
with:
context: .
platforms: linux/amd64
push: false
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Show Docker images
run: docker images
+3 -3
View File
@@ -14,13 +14,13 @@ jobs:
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4.2.0
- name: NPM Lockfile Changes
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891 # v1.0.0
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Optional inputs, can be deleted safely if you are happy with default values.
collapsibleThreshold: 25
failOnDowngrade: false
path: package-lock.json
updateComment: true
updateComment: true
+5 -5
View File
@@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4.2.0
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'
package-manager-cache: false
node-version: 20.12.2
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build app
@@ -24,7 +24,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
with:
publish-dir: dist
deploy-message: 'Dev deploy ${{ github.sha }}'
-15
View File
@@ -1,15 +0,0 @@
name: Check PR title
on:
pull_request_target:
types:
- opened
- edited
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+17 -17
View File
@@ -10,12 +10,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4.2.0
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'
package-manager-cache: false
node-version: 20.12.2
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build app
@@ -23,7 +23,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
with:
publish-dir: dist
deploy-message: 'Prod deploy ${{ github.ref_name }}'
@@ -52,45 +52,45 @@ jobs:
gpg --export | xxd -p
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
- name: Upload tagged release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8
with:
files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
publish-image:
name: Push Docker image to Docker Hub, GHCR
name: Push Docker image to Docker Hub, ghcr
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4.2.0
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Login to Docker Hub #Do not update this action from a outside PR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/setup-buildx-action@v3.10.0
- name: Login to Docker Hub
uses: docker/login-action@v3.4.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to the Github Container registry #Do not update this action from a outside PR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
- name: Login to the Container registry
uses: docker/login-action@v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker, GHCR
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
uses: docker/metadata-action@v5.7.0
with:
images: |
${{ secrets.DOCKER_USERNAME }}/cinny
ghcr.io/${{ github.repository }}
- name: Build and push Docker image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64
+1 -2
View File
@@ -4,5 +4,4 @@ node_modules
devAssets
.DS_Store
.ideapackage-lock.json
public/decorations/
.idea
-3
View File
@@ -1,3 +0,0 @@
# These are commented until we enable lint and typecheck
# npx tsc -p tsconfig.json --noEmit
# npx lint-staged
-1
View File
@@ -1 +0,0 @@
24.13.1
+1 -2
View File
@@ -1,3 +1,2 @@
legacy-peer-deps=true
save-exact=true
@lotusguild:registry=https://code.lotusguild.org/api/packages/LotusGuild/npm/
save-exact=true
+11 -11
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
+4 -7
View File
@@ -5,7 +5,6 @@ First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
>
> - Star the project
> - Tweet about it (tag @cinnyapp)
> - Refer this project in your project's readme
@@ -19,8 +18,7 @@ Bug reports and feature suggestions must use descriptive and concise titles and
## Pull requests
> ### Legal Notice
>
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. You will also be asked to [sign the CLA](https://github.com/cinnyapp/cla) upon submiting your pull request.
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.
@@ -28,9 +26,9 @@ Bug reports and feature suggestions must use descriptive and concise titles and
Example:
| Not ideal | Better |
| ----------------------------------- | --------------------------------------------- |
| Fixed markAllAsRead in RoomTimeline | Fix read marker when paginating room timeline |
|Not ideal|Better|
|---|----|
|Fixed markAllAsRead in RoomTimeline|Fix read marker when paginating room timeline|
It is not always possible to phrase every change in such a manner, but it is desired.
@@ -41,7 +39,6 @@ Also, we use [ESLint](https://eslint.org/) for clean and stylistically consisten
**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
## Helpful links
- [BEM methodology](http://getbem.com/introduction/)
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
+2 -2
View File
@@ -1,5 +1,5 @@
## Builder
FROM node:24.13.1-alpine AS builder
FROM node:20.12.2-alpine3.18 as builder
WORKDIR /src
@@ -11,7 +11,7 @@ RUN npm run build
## App
FROM nginx:1.29.8-alpine
FROM nginx:1.29.0-alpine
COPY --from=builder /src/dist /app
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
-666
View File
@@ -1,666 +0,0 @@
# HANDOFF — Forking & Self-Building Element Call ("Lotus Call")
> **Audience:** a fresh Claude/engineer session with **no prior context** on this
> project. Read this top-to-bottom before touching anything. This document is the
> single source of truth for the Element Call (EC) fork initiative.
>
> **Status:** **PHASE 02 IMPLEMENTED (build-verified, not yet live-tested)**
> (2026-06-30). The fork exists, builds, is published, and cinny consumes it
> (Phase 0/1). **All 7 Phase-2 EC features are implemented on the fork's `lotus`
> branch**, each additive + flag-gated, build+typecheck-clean, per-feature
> reviewed (+ a holistic multi-agent review), and pushed. **None are live-tested
> yet** — every one needs the `LOTUS_TESTING.md` §D sweep, and the **cinny host
> side must be wired** (set flags / send actions / handle call_state) — see §12.
> See **§9** Phase 0/1 results, **§10** cutover, **§11** Phase-2 seams, **§12**
> Phase-2 status + cinny integration checklist. Created 2026-06 from `LotusGuild/cinny`.
---
## 9. Phase 0 Results (verified 2026-06-29)
**Decisions taken with the user:** scope = Phase 0 recon; consumption model =
**private npm package** (§5 option 1). Recommended registry = **Gitea's built-in
npm registry** (`code.lotusguild.org`) — zero new infra.
### 9.1 Version → tag → commit mapping (LOCKED)
| Source | Value |
| :--------------------------------------------------- | :----------------------------------------- |
| cinny `package.json` pin | `@element-hq/element-call-embedded@0.20.1` |
| Bundle self-report (`VITE_APP_VERSION`/`appVersion`) | `embedded-v0.20.1` |
| npm registry `gitHead` for 0.20.1 | `2d74c48151d9edc01c65a22a91478aac81bf24d0` |
| GitHub tag `v0.20.1` → commit | `2d74c48…`**same commit** |
**Fork from upstream tag `v0.20.1` (commit `2d74c48`).** The embedded package
version equals the element-call release tag; repo `package.json` version is
`0.0.0` and the real version is stamped at publish time from the tag.
### 9.2 The shipped npm dist is a CLEAN upstream build
No `lotus`/`denoise`/`rnnoise` strings anywhere in
`node_modules/@element-hq/element-call-embedded/dist`. **All Lotus customization
(denoise shim) is injected at cinny build time, not baked into the package** — so
swapping the source does not disturb cinny's denoise injection layer. The
ringtone/reaction assets (`baduntss`, `cat`, `clap`, `call_declined`, …) are
upstream EC's own, not ours.
### 9.3 Build toolchain & mechanism
- **Node `24`** (`.node-version`), **pnpm `10.33.0`** (`packageManager` field,
via corepack).
- Build: **`pnpm run build:embedded`** = `vite build --config
vite-embedded.config.ts` with `NODE_OPTIONS=--max-old-space-size=16384`.
- Output dir is **repo-root `dist/`**; CI stages it into **`embedded/web/dist`**
(the `embedded/web/` dir holds the publish template: `package.json`, README,
both LICENSE files).
- Publish workflow upstream = `.github/workflows/publish-embedded-packages.yaml`:
builds → `npm version <tag> --no-git-tag-version` → `npm publish --provenance
--access public` to npmjs as `@element-hq/element-call-embedded`. (Also
Android/Maven + iOS/SwiftPM — irrelevant; we are web-only.)
### 9.4 Build reproduction — PARITY CONFIRMED
Cloned `element-call@v0.20.1` to `/root/code/element-call` (shallow), built with
isolated Node 24 / pnpm 10.33.0 (system Node 20 / cinny untouched). Result vs the
shipped npm dist:
- **137 of 147 files byte-identical** (same Vite content-hash): all CSS, fonts,
wasm, audio, JSON locale files, and `IndexedDBWorker`.
- **Only 5 JS chunks differ** (`index`, `pako.esm`, `polyfill-force`,
`rust-crypto`, `spa`) — **cause isolated to the version define**: our local
build baked `appVersion:\`dev\``(because`VITE_APP_VERSION`was unset) vs the
npm build's`appVersion:\`embedded-v0.20.1\``. `index.html` is identical modulo
the hashed asset filenames. **Benign** — our CI sets the version from the git
tag, so a tagged CI build will match.
### 9.5 Fork CI (drafted)
`.gitea/workflows/ci.yml` is staged in the clone (models cinny's
`.gitea/workflows/ci.yml` + upstream's publish flow). Linux-only (`ubuntu-latest`)
— the Windows worker is for cinny-desktop/Tauri, not the EC web bundle. Build job
on PR/push to `lotus`; publish job on `v*` tag → `@lotusguild/element-call-embedded`
to the Gitea npm registry (needs `secrets.GITEA_NPM_TOKEN`).
### 9.6 Phase 1 — DONE (2026-06-29)
1. ✅ **Fork repo live:** `code.lotusguild.org/LotusGuild/element-call` (public,
AGPL), default branch `lotus`, full history (7018 commits) + tag `v0.20.1`.
Branch `lotus` = `v0.20.1` + 2-file diff (CI workflow + embedded package
rename).
2. ✅ **Package published:** `@lotusguild/element-call-embedded@0.20.1` on the
Gitea npm registry (published manually from the version-faithful build while
the admin token was available). **Publicly readable** (unauth `npm install`
works → devs/CI need no token to consume; only publishing needs one).
3. ✅ **cinny wired & built clean** (Node 24): `.npmrc` scope line +
`package.json` dep + `vite.config.js` `viteStaticCopy` src. `npm install`
swapped the package (resolved from Gitea), `npm run build` succeeded,
`dist/public/element-call/` populated, bundle reports `appVersion:
embedded-v0.20.1`, **denoise shim injected + all denoise assets copied**
(injection layer unchanged). **These cinny edits are staged in the working
tree, NOT committed/pushed** — pushing triggers CI → desktop → deploy, so it's
gated on the §D live test (see §10).
### 9.8 Reproducibility note (important)
A from-source rebuild is **NOT byte-identical** to upstream's npm tarball.
137/147 files match exactly (CSS, fonts, wasm, audio, worker); the 5 JS chunks
(`index`, `pako.esm`, `polyfill-force`, `rust-crypto`, `spa`) differ because the
rolldown/oxc **minifier mangles export names differently** across build
environments (and the version-define is one input). This is normal and benign —
the code is functionally equivalent. **Do not chase byte-parity; the §D live call
test is the real parity gate.**
### 9.9 Remaining follow-ups (not blocking the cutover)
- **CI publishing:** `.gitea/workflows/ci.yml` publishes on a `v*` tag but needs
(a) a Gitea Actions runner for `LotusGuild/element-call`, and (b) a **durable**
`GITEA_NPM_TOKEN` repo secret with package read/write (the admin token used for
the manual publish is being deleted, so it was deliberately NOT baked in). Until
then, publishing is manual (`npm version <tag>` in `embedded/web` →
`npm publish`).
- Decide rebase cadence vs upstream (0.20.2 / 0.20.3 already out — see §9.1).
### 9.7 Ready-to-apply artifacts (staged 2026-06-29)
**Fork side — already committed** on branch `lotus` in `/root/code/element-call`
(remote `lotus` = `code.lotusguild.org/LotusGuild/element-call.git`, push deferred
until the repo exists). Minimal 2-file diff vs tag `v0.20.1`:
`.gitea/workflows/ci.yml` (new) + `embedded/web/package.json` (rename to
`@lotusguild/element-call-embedded`). Push with:
`git push -u lotus lotus && git push lotus v0.20.1` (and tag `v0.20.1` on our side
to trigger the first publish, or push our own `v0.20.1` tag).
**cinny side — NOT yet applied** (applying before the package is published breaks
`npm ci`). Exactly 3 edits + a lockfile regen:
1. `.npmrc` — append the scoped-registry line:
```
@lotusguild:registry=https://code.lotusguild.org/api/packages/LotusGuild/npm/
```
(CI/auth: `//code.lotusguild.org/api/packages/LotusGuild/npm/:_authToken=${GITEA_NPM_TOKEN}`
— inject via env in CI, do not commit a plaintext token.)
2. `package.json:104` —
`"@element-hq/element-call-embedded": "0.20.1"` →
`"@lotusguild/element-call-embedded": "0.20.1"`.
3. `vite.config.js:25` — `viteStaticCopy` src:
`node_modules/@element-hq/element-call-embedded/dist` →
`node_modules/@lotusguild/element-call-embedded/dist`.
**`stripBase: 4` stays unchanged** — `node_modules/@lotusguild/element-call-embedded/dist`
is still exactly 4 leading segments. (Update the comment's path reference too.)
4. `package-lock.json` — regenerated by `npm install`, not hand-edited (drops the
`registry.npmjs.org/@element-hq/...` resolved URL for the Gitea one).
The denoise injection (`lotusDenoise()` in `vite.config.js`) is **unchanged** — it
keys off `dist/public/element-call/index.html`, which our fork's bundle still
produces identically (verified: `index.html` byte-identical modulo asset hashes).
---
## 0. TL;DR / The Goal
We embed **Element Call** (the Matrix group-VoIP/video app) inside Lotus Chat to
power voice/video channels. Today we consume Element's **pre-compiled npm
bundle** and can only steer it from the outside (a limited widget API + fragile
same-origin DOM hacks). Several in-call problems are **unfixable from outside**
because they live in EC's compiled JS.
**We want true ownership: fork `element-hq/element-call`, build it from source
ourselves, host our build, and replace the npm bundle with our fork.** Then
every in-call behavior becomes editable code.
**This requires standing up a brand-new repo and build pipeline for our EC fork.**
---
## 1. Why fork? (What we cannot fix today)
These came out of live testing and are documented in `LOTUS_BUGS.md` →
"Known Element Call iframe limitations":
| Issue | What's wrong | Why outside-fixes fail |
| :----------------------------------------------------- | :------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A6** — avatar decorations in-call | Our profile-decoration overlays don't appear on in-call video tiles | The video grid is rendered by EC's React app inside the iframe. We can only inject overlay DOM (fragile) — we can't make it a first-class part of the tile. |
| **A5** — focus camera / fullscreen during screenshare | Can't reliably spotlight a participant's camera while someone screenshares | EC's **layout logic** (screenshare priority, spotlight) is compiled JS we don't control. We currently DOM-click tiles as a hack. |
| **A7** — mic dead after EC's "Reconnect" | After EC's own mid-call reconnect, the local mic isn't re-published | EC's reconnect/track-republish path is internal. (Partly entangled with our denoise shim — see §6.) |
| Native theming | EC's UI doesn't match Lotus design; we inject CSS hacks | Real theming needs source-level component/token changes. |
| Decorations, custom controls, custom layouts, branding | all blocked | all require source access |
**Bottom line:** the iframe is **same-origin** (we self-host it), so we can read
and even write its DOM — but we **do not own its source**, so we can't change its
**behavior/logic**, only poke at its rendered output. Forking removes that wall.
---
## 2. How EC is integrated TODAY (the current architecture)
Understand this fully before changing it — the fork must slot into the same
integration seams.
### 2.1 Where the EC bundle comes from
- npm package: **`@element-hq/element-call-embedded`**, pinned to **`0.20.1`** in
`cinny/package.json` (line ~104).
- It ships a **pre-built `dist/`**. At cinny build time,
`vite-plugin-static-copy` copies that `dist/` flat into
**`public/element-call/`** (see `cinny/vite.config.js`, the `copyFiles`
target with `rename: { stripBase: 4 }` — note the stripBase gotcha documented
there; getting this wrong 404s the widget).
- It is **NOT committed** to git (`git ls-files public/element-call` → 0). It's a
build artifact materialized from `node_modules`.
### 2.2 How EC is loaded & controlled
- The widget iframe `src` is **same-origin**:
`${BASE_URL}/public/element-call/index.html?<params>` (see
`cinny/src/app/plugins/call/CallEmbed.ts`, `getWidget()` /
`getIframe()`). Sandbox: `allow-forms allow-scripts allow-same-origin
allow-popups allow-modals allow-downloads`; `allow="microphone; camera;
display-capture; autoplay; clipboard-write;"`.
- **Control surface #1 — the official widget API** (`matrix-widget-api`):
`ClientWidgetApi` + a custom `CallWidgetDriver`. This is the robust,
version-stable channel (theme change, hangup, capabilities, timeline events).
Files: `plugins/call/CallEmbed.ts`, `plugins/call/CallWidgetDriver.ts`,
`plugins/call/utils.ts` (capabilities), `plugins/call/CallControl.ts`.
- **Control surface #2 — same-origin DOM poking** (fragile, version-coupled):
reading `iframe.contentDocument` to detect speakers/mute state and
`.click()`-ing tiles to focus a camera. Files:
`hooks/useCallSpeakers.ts` (reads `[data-muted]`, `[data-video-fit]`),
`plugins/call/CallControl.ts` (`focusCameraParticipant` — tile selectors).
**These selectors break on every EC version bump.** A fork lets us replace
these hacks with real APIs/props.
- **Control surface #3 — URL params + build-time injection** for our denoise
shim (see §6).
### 2.3 Full file inventory (everything that touches EC in cinny)
Plugin / core:
- `src/app/plugins/call/CallEmbed.ts` — iframe creation, widget API wiring, theme sync, hangup, load watchdog/self-heal, denoise URL params.
- `src/app/plugins/call/CallControl.ts` — control state + **DOM-poking** (`focusCameraParticipant`, spotlight).
- `src/app/plugins/call/CallControl.tsx` _(call-status variant)_ and `features/call-status/CallControl.tsx`.
- `src/app/plugins/call/CallWidgetDriver.ts` — widget driver (capabilities, event relay).
- `src/app/plugins/call/utils.ts` — widget capabilities set.
- `src/app/plugins/call/hooks.ts`, `index.ts` — plugin exports/hooks.
- `src/app/state/callEmbed.ts` — jotai atoms for the active embed.
React / UI:
- `src/app/components/CallEmbedProvider.tsx` — the big one: incoming-call ring/banner, RTCNotification + **RTCDecline** listeners, PiP, mute badges, fullscreen, ringtones.
- `src/app/features/call/CallView.tsx` — prescreen lobby vs joined (the iframe placement target), load-error recovery UI.
- `src/app/features/call/CallControls.tsx` — in-call control bar (mic/cam/deafen/screenshare/fullscreen/more/PiP).
- `src/app/features/call/CallMemberCard.tsx` — **lobby** participant roster (this is where `AvatarDecoration` works today; in-call grid is EC's).
- `src/app/features/call/PrescreenControls.tsx` — join controls.
- `src/app/features/call-status/*` — `CallStatus.tsx`, `MemberGlance.tsx` (the "Focus camera" menu lives here), `LiveChip.tsx`.
- `src/app/features/room-nav/RoomNavItem.tsx`, `features/room/Room.tsx`, `features/room/RoomViewHeader.tsx`, `pages/client/space/Space.tsx`, `pages/CallStatusRenderer.tsx`, `pages/Router.tsx` — call entry points / status surfacing.
Hooks:
- `src/app/hooks/useCallEmbed.ts`, `useCall.ts`, `useCallSpeakers.ts` (DOM-poking), `useCallJoinLeaveSounds.ts`, `useAfkAutoMute.ts`.
Build:
- `cinny/vite.config.js` — `copyFiles` (EC dist copy) + `lotusDenoise()` plugin (denoise asset copy + index.html shim injection, in `closeBundle`).
Utils:
- `src/app/utils/ringtones.ts`, `utils/denoisePipeline.ts`, `utils/lotusDenoiseUtils.ts`.
---
## 3. Hosting / infra context (the OTHER repo)
There are **two repos**:
1. **`LotusGuild/cinny`** (`/root/code/cinny`) — this Lotus Chat fork. Consumes EC.
2. **`LotusGuild/matrix`** (`/root/code/matrix`) — the **infra/homeserver** repo.
Subdirs: `livekit/` (the SFU EC talks to), `deploy/`, `draupnir/`,
`hookshot/`, `landing/`, `matrixbot/`, `systemd/`. Gitea remote
`code.lotusguild.org/LotusGuild/matrix`, branch `main`.
EC needs a **LiveKit SFU** + the **livekit-jwt-service**; those live in
`matrix/livekit/`. A self-hosted EC build must be configured to point at our
homeserver (`matrix.lotusguild.org` / synapse) and our LiveKit. EC's runtime
`config.json` (homeserver, livekit URL, feature flags) is part of what we'll own
once we build it ourselves.
Deployment today: `chat.lotusguild.org` (the cinny web build, which embeds EC at
`/public/element-call/`). cinny-desktop (`LotusGuild/cinny-desktop`, a Tauri
wrapper, bumped by cinny CI) embeds the same.
---
## 4. The plan (proposed — confirm with the user before executing)
### Decision: **YES, create a new repo.** `LotusGuild/element-call`
Rationale: EC is a large standalone app (React + LiveKit client SDK + matrixRTC +
its own Vite build + heavy deps). Keep it out of cinny so cinny's build stays
clean — cinny keeps consuming a **built EC `dist/`**, exactly as today, just
sourced from **our fork** instead of npm.
### Phase 0 — Recon (no code)
- Fork `github.com/element-hq/element-call` → `LotusGuild/element-call` on Gitea.
- Pin to the upstream tag matching **0.20.1** (`element-call-embedded` 0.20.1's
corresponding `element-call` release) so behavior matches what's shipping now.
Verify the embedded-package version ↔ element-call repo tag mapping.
- Read EC's own build docs: it builds the "embedded" widget bundle (the thing
currently published as `@element-hq/element-call-embedded`). Reproduce that
build locally and confirm the output matches `public/element-call/` today.
- **License:** element-call is **AGPL-3.0**, same as Lotus Chat — compatible.
Our fork must remain AGPL and publish source.
### Phase 1 — Reproduce current behavior from our fork (parity, no features)
- Build our fork's embedded bundle; wire cinny to consume it instead of the npm
package (see §5 for the consumption options). Smoke-test: a call works exactly
as today (web + desktop), denoise shim still injects, widget API + theme still
work. **No behavior change yet** — this de-risks the swap.
### Phase 2 — Replace the outside hacks with source-level features
Tackle the §1 issues in EC's source:
- **A6:** render avatar decorations as part of the video-tile component
(read decoration data we pass in via widget data / URL param / a small bridge).
- **A5:** fix focus/spotlight + screenshare-coexistence in EC's layout code;
expose a clean widget action so cinny can trigger it (kill the DOM `.click()`).
- **A7:** fix mic re-publish on reconnect; reconcile with our denoise shim (§6) —
ideally move denoise INTO the fork as a real audio-processing step instead of a
`getUserMedia` monkeypatch.
- Native Lotus theming/branding at the source (kill the injected-CSS hacks).
- Then retire the DOM-poking in `useCallSpeakers.ts` / `CallControl.ts` in favor
of real widget messages.
### Phase 3 — Maintenance posture
- Decide rebase cadence vs. upstream element-call releases. Keep customizations
isolated (feature flags / minimal-diff patches) to ease rebasing.
- CI in the new repo builds + publishes the embedded dist as a versioned
artifact; cinny CI consumes a pinned version.
---
## 5. How cinny should consume the fork (pick one — decide with user)
1. **Private npm package** (mirror the current model): our fork's CI publishes
`@lotusguild/element-call-embedded` to a registry; cinny depends on it and
`viteStaticCopy` keeps working almost unchanged. _Cleanest swap; needs a
registry._
2. **Git submodule + build in cinny CI:** add the fork as a submodule, build it
during cinny's build, copy its `dist/` to `public/element-call/`. _No
registry; heavier cinny CI._
3. **CI artifact copy:** fork CI uploads a `dist` tarball; cinny CI downloads a
pinned version at build. _Decoupled; needs artifact plumbing._
**Recommendation: Option 1** — it changes the least in cinny (just swap the
package name in `package.json` + the `viteStaticCopy` src path) and preserves the
clean cinny/EC separation.
---
## 6. The denoise shim — critical interaction (don't break this)
Lotus ships ML noise suppression by **injecting a same-origin pre-init shim into
EC's `index.html` at build time** (cinny `vite.config.js` → `lotusDenoise()`,
`closeBundle`). The shim monkeypatches `getUserMedia` **before EC captures the
mic** and routes audio through RNNoise/Speex/DTLN AudioWorklets, then EC/LiveKit
publishes the processed track. It's activated via URL params
(`lotusDenoise=ml&lotusModel=…&lotusGate=…`) set in `CallEmbed.ts`.
- Assets copied to `public/element-call/denoise/` at build (sapphi RNNoise/Speex/
gate worklets + `@workadventure/noise-suppression` DTLN tree).
- Related: `utils/denoisePipeline.ts`, `utils/lotusDenoiseUtils.ts`,
`settings/general/DenoiseTester.tsx`, `VoiceMessageRecorder.tsx`.
- **Known issues:** denoise quality is still poor (tracked separately); and the
mic-after-reconnect bug (A7) is suspected to involve the shim's getUserMedia
patch handing back a stale processed stream when EC re-acquires the mic.
**Once we own the fork, the right move is to make denoise a first-class
audio-processing stage inside EC** (not an index.html monkeypatch) — more robust,
survives reconnects, and removes the build-time injection hack. Until then, the
fork's `index.html` must remain injectable the same way, or the shim must be
re-homed into the fork.
---
## 7. Doc-accuracy notes / corrections for the new session
- `LOTUS_TODO.md` (~line 533) calls EC a **"cross-origin iframe"** — **outdated.**
EC is **same-origin** today (self-hosted under our domain;
`iframe.sandbox` includes `allow-same-origin`; we read `contentDocument`), and
**as of 2026-06-29 we own the fork's source** (`@lotusguild/element-call-embedded`).
The _practical_ point it made still holds _until we ship the audio-inject API_:
**LiveKit's `LocalAudioTrack` lives in EC's module scope**, not on `window`, so
cinny can't reach it even same-origin — which is why the in-call soundboard had
to be local-playback-only. **The fork removes this wall:** EC can expose a real
`io.lotus.inject_audio` widget action (Phase 2) that mixes into the published
track from inside its own module scope.
- `LOTUS_FEATURES.md` documents the EC upgrade history (0.16.3 → 0.19.4 →
0.20.1), the dark-mode CSS injection, and AFK auto-mute — all relevant prior
art for what the fork must preserve.
- `LOTUS_TESTING.md` §D is the **EC regression sweep** to re-run after the fork
swap (Phase 1 parity check).
---
## 8. First actions for the new session
1. Read this file, then skim §2.3's files in `cinny` to internalize the seams.
2. Confirm with the user: new repo name, consumption model (§5), rebase cadence.
3. Phase 0: fork element-call, map 0.20.1 ↔ element-call tag, reproduce the
embedded build locally, diff against `public/element-call/`.
4. Phase 1: wire cinny to the fork, run `LOTUS_TESTING.md` §D parity sweep.
5. Only then start Phase 2 features (A5/A6/A7, theming, denoise-in-source).
**Cross-references:** `LOTUS_BUGS.md` (EC limitations + verify queue),
`LOTUS_TODO.md` (denoise/soundboard constraints), `LOTUS_FEATURES.md` (EC history),
`LOTUS_TESTING.md` §D (regression sweep). Infra: `/root/code/matrix` (`livekit/`,
`deploy/`).
---
## 10. Live cutover — the remaining steps (Phase 1 finish)
The fork is published and cinny builds against it locally (§9.6). What's left to
go live:
1. **Run `LOTUS_TESTING.md` §D** against a local cinny build (`npm run build` is
already proven; serve `dist/` or `npm run dev`). Verify a real call: join,
mic/cam, screenshare, theme sync, denoise on, widget hangup — web first.
2. **Commit the cinny edits** (currently staged, uncommitted in the working tree):
`.npmrc`, `package.json`, `package-lock.json`, `vite.config.js`. Suggested
message: `chore(call): consume self-built @lotusguild/element-call-embedded`.
3. **Push to `lotus`** → cinny CI builds, then `trigger-desktop` bumps
cinny-desktop → Tauri release. Re-run §D on **cinny-desktop** (the path where
the old `stripBase` bug bit — verify the widget loads, not a 404).
4. Only then start **Phase 2** (A5/A6/A7, theming, denoise-in-source).
---
## 11. Phase 2 — implementation seams (mapped 2026-06-29)
The exact integration points for each Phase 2 item, found by reading the EC fork
- cinny source. **All of these are media-path / in-call features that cannot be
functionally verified without a live Matrix + LiveKit call** — implement each as
a minimal, **feature-flagged, additive** diff (no behavior change unless cinny
opts in), build-verify the fork (`pnpm build:embedded`, ~15s) AND cinny
(`npm run build`), then gate shipping on `LOTUS_TESTING.md` §D.
**Shared widget channel (the backbone for #2/#3/#4/#7):**
- EC→cinny: `widget.api.transport.send("io.lotus.<x>", data)` (see
`element-call/src/widget.ts`).
- cinny→EC actions: add the action name to the `lazyActions` allow-list in
`widget.ts` (the array at ~L101) and handle it in EC; cinny sends via
`this.call.transport.send(...)`.
- cinny receives EC→cinny actions via the existing `listenAction(type, cb)`
helper in `plugins/call/CallEmbed.ts:626` (auto-replies `{}` so the transport
doesn't time out — same pattern as `io.element.device_mute`).
**#2 mute/speaker events** — Source: subscribe to `vm.userMedia$`
(`CallViewModel`), per member `speaking$` + `audioEnabled$`
(`state/media/UserMediaViewModel.ts:47-48`); aggregate and
`transport.send("io.lotus.call_state", {participants:[{id,speaking,audioEnabled}]})`.
Mount in `room/InCallView.tsx` via `useEffect` guarded by `widget !== null`.
cinny: `listenAction("io.lotus.call_state")` in `CallEmbed.ts`, feed
`hooks/useCallSpeakers.ts` → delete its `contentDocument` `[data-muted]` /
`[data-video-fit]` scrape. _Additive, low risk._
**#4 spotlight/focus** — EC: add `io.lotus.focus_participant` to the `lazyActions`
list (`widget.ts`), drive `vm`'s spotlight (`spotlightSpeaker$` /
`spotlight$` in `CallViewModel.ts:898/1001`) to pin a given identity, coexisting
with `hasRemoteScreenShares$` (L1008). cinny: replace
`CallControl.ts` `focusCameraParticipant` `.click()` walk with
`transport.send("io.lotus.focus_participant", {userId})`. _Additive, low risk._
**#3 audio-inject** — EC: add `io.lotus.inject_audio` action; mix an
`AudioBufferSourceNode` into the published mic track. The local publish path is
`state/CallViewModel/localMember/Publisher.ts` + `LocalMember.ts` (LiveKit
`localParticipant`); create a `MediaStreamAudioDestinationNode`, mix mic + clip,
`replaceTrack`. cinny soundboard calls the action instead of local-only playback.
_Medium; touches publish path → live-test carefully._
**#1 denoise-in-source** — replace the cinny `lotusDenoise()` `getUserMedia`
monkeypatch with a real processing stage in EC's mic capture
(`Publisher.ts`/`LocalMember.ts`; note EC has a `TrackProcessorContext` +
`BlurBackgroundTransformer` precedent in `livekit/`). EC re-runs it on every
(re)publish → fixes A7. Remove `vite.config.js` `lotusDenoise()` + URL params in
`CallEmbed.ts`; move `denoise/` assets into the fork. _Highest value, highest
risk — most live testing._
**#5 theming** — add a Lotus/TDS theme in EC's theme system (`src/useTheme.ts` +
EC theme tokens / CSS); driven by the existing `setTheme()` channel cinny already
calls (`CallEmbed.ts:277`). Bake transparent background. Delete cinny's
`applyStyles()` injection + `background:none !important`. _Medium._
**#6 in-call decorations** — render the decoration APNG in EC's tile component
(`tile/GridTile.tsx`); pass slugs via widget member data. cinny already has the
decoration data + `AvatarDecoration` (lobby `CallMemberCard.tsx`). _Medium-Large._
**#7 quality controls** — set audio `maxBitrate` via
`RTCRtpSender.setParameters` and screenshare `getDisplayMedia` constraints in
EC's publish path (`Publisher.ts`); configurable via `config.json` / a widget
message. Keep the server `voice-limit-guard` as enforcement. _Medium._
**Rollback:** revert the 4 cinny files (restores `@element-hq/...@0.20.1` from
npmjs). The fork repo/package can stay; nothing else depends on it until pushed.
### Local repro/build environment (this session, 2026-06-29)
- Upstream cloned + our `lotus` branch at `/root/code/element-call` (remote
`lotus` → Gitea; origin → github upstream, now un-shallowed/full history).
- Isolated **Node 24.18.0** lives in the session scratchpad (system Node is 20);
cinny's `.node-version` is `24.13.1`, so use Node 24 to build cinny too.
- Build the embedded bundle: in `/root/code/element-call`, with Node 24 + pnpm
10.33.0 on PATH, `VITE_APP_VERSION=embedded-v0.20.1 pnpm run build:embedded`
→ output in `dist/`; stage to `embedded/web/dist` before publishing.
---
## 12. Phase 2 — IMPLEMENTED on the fork (2026-06-30)
All 7 EC features are on the `lotus` branch of `LotusGuild/element-call`, each
**additive + feature-flagged** (a vanilla call with no `lotus*` params / no Lotus
actions behaves exactly like upstream), build + `tsc` clean, per-feature reviewed
(fixes applied) and holistically reviewed. **Not yet live-tested** — all need the
`LOTUS_TESTING.md` §D sweep.
Fork modules live under `element-call/src/lotus/*`; mounts are `useEffect`s in
`src/room/InCallView.tsx`. Custom widget actions are in `src/lotus/lotusActions.ts`
(toWidget ones allow-listed in `src/widget.ts`).
| # | Feature | Enable via | EC module |
| :-- | :------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------ | ---------------------------------------------------- |
| 2 | Speaker/mute/camera state → host | URL `lotusCallState=1` | `lotusCallState.ts` (sends `io.lotus.call_state`) |
| 4 | Focus/spotlight a participant (works during screenshare) | action `io.lotus.focus_participant {userId | null}` | `lotusFocus.ts` + `CallViewModel` spotlight override |
| 3 | Soundboard audio-inject (heard by peers) | URL `lotusAudioInject=1` + action `io.lotus.inject_audio {url,volume?}` | `lotusAudioInject.ts` |
| 7 | Audio/screenshare quality caps | action `io.lotus.set_quality {audioMaxBitrate?,screenshareMaxBitrate?,screenshareMaxFramerate?}` | `lotusQuality.ts` |
| 5 | Transparent bg + Lotus theme | URL `lotusTransparent=1` / `lotusTheme=1` | `useTheme.ts` + `index.css` |
| 6 | In-call avatar decorations | action `io.lotus.decorations {decorations:{userId:url}}` | `lotusDecorations.ts` + `MediaView.tsx` |
| 1 | ML denoise in-source (fixes A7) | URL **`lotusDenoiseSource=1`** (+`lotusModel`,`lotusGate`,`lotusGateThreshold`,`lotusDenoiseBase`) — deliberately NOT the existing `lotusDenoise=ml` (that drives the host shim; reusing it would double-process) | `lotusDenoise.ts` + `lotusDenoiseProcessor.ts` |
**Security hardening applied** (holistic audit): `lotusDenoiseBase` forced
same-origin before `audioWorklet.addModule` (was an arbitrary-code-load vector
via a crafted link); audio-inject gated behind `lotusAudioInject=1`; decoration
roster capped. Only `https`/`blob` URLs accepted for inject/decoration assets.
### 12.1 cinny host integration checklist (REQUIRED to light these up)
> ✅ **STATUS (2026-06): COMPLETE.** All items below are shipped. call_state,
> focus_participant, decorations, and transparent background are active; the
> in-source denoise cutover is done (flag `lotusDenoiseSource=1`, **all four**
> models in-source); and the two formerly-dormant capabilities now have cinny
> UI — **soundboard** (`io.lotus.inject_audio`, P5-15) and **quality controls +
> room permissions** (`io.lotus.set_quality` + `io.lotus.room_quality`, P5-31,
> with server-side enforcement in `LotusGuild/matrix`). See `LOTUS_FEATURES.md`
> → "Element Call — Self-Built Fork". The checklist is kept below as the record
> of what was wired. (One open denoise item tracked separately: the "Series
> Suppression" native-NS toggle is not wired to the real call path.)
The EC side is additive and dormant until cinny opts in. Host work (in
`src/app/plugins/call/CallEmbed.ts` unless noted) — **done**:
> ⚠️ **CRITICAL TIMING (protocol audit F1):** only send `io.lotus.*` **toWidget**
> actions (#3 focus, #6 decorations, #7 quality, audio-inject) **after** the call
> is joined (`CallEmbed.onCallJoined` / `this.joined`). Those actions are
> allow-listed at EC app-init (so `preventDefault` suppresses the auto-error)
> but their handlers only mount with `InCallView` (post-join). Sending earlier
> leaves the host's `transport.send` pending until the **10s timeout**. Queue and
> flush on join, or no-op before join.
>
> Also: **F3 (RESOLVED)** — all four models (`rnnoise`/`speex`/`dtln`/
> `deepfilternet`) are now implemented in-source in `lotusDenoiseProcessor.ts`;
> the picker offers all four. **F4** — cinny no longer forwards a native-NS flag
> in the `ml` branch (the "Series Suppression" toggle is currently a no-op in
> real calls — open item). **F7** — no widget _capability_ changes needed;
> custom actions bypass capability checks.
1. **Set the URL flags** on the widget iframe params (the `URLSearchParams` in
`CallEmbed`): `lotusCallState=1`, `lotusTransparent=1`/`lotusTheme=1`,
`lotusAudioInject=1` as desired. (Denoise sets `lotusDenoiseSource=1` + `lotusModel`/`lotusGate`/`lotusGateThreshold` in the `ml` tier.)
2. **Ack `io.lotus.call_state`**: add `listenAction('io.lotus.call_state', …)` —
without a reply the fork's sends time out every 250ms. Feed the payload into
`useCallSpeakers` and RETIRE its `contentDocument` DOM scrape.
3. **Send actions** via `this.call.transport.send(...)`:
`io.lotus.focus_participant` (replace `CallControl.focusCameraParticipant`s
`.click()`), `io.lotus.inject_audio` (from the soundboard), `io.lotus.set_quality`
(from quality settings), `io.lotus.decorations` (push the MSC4133 decoration
map; resolve mxc→https first).
4. **#1 denoise cutover**: once verified, STOP injecting the `lotusDenoise()`
shim in `cinny/vite.config.js` and remove the `index.html` injection — the
fork now does denoise in-source. Keep shipping the `denoise/` assets (the
fork loads `./denoise/…` at runtime) until those move into the fork build.
5. Re-run `LOTUS_TESTING.md` §D for each feature; only then ship.
### 12.2 Holistic multi-agent review — outstanding follow-ups (non-blocking)
Four aspect-agents reviewed the whole fork. Criticals were fixed in-branch (the
denoise restart-silence/A7 bug; the `lotusDenoiseBase` code-load vector;
audio-inject opt-in gate; #6 rendering in the wrong component; #7 simulcast cap).
Remaining, deliberately deferred:
- **Denoise H2 (double-processing):** if cinny is set to `lotusDenoise=ml` while
ALSO still injecting its build-time `getUserMedia` shim, audio is denoised
twice. The #1 cutover MUST remove the cinny-side injection (it currently has
none injected into the iframe — keep it that way). Hard requirement, not code.
- **Denoise M1 (perf):** in-source uses non-SIMD `rnnoise.wasm`; the reference
preferred SIMD with detection. Perf-only; add SIMD detection later.
- **dtln/deepfilternet (F3): RESOLVED** — all four models
(rnnoise/speex/dtln/deepfilternet) are now implemented in
`lotusDenoiseProcessor.ts` (faithful port of cinny's `build/lotus-denoise.js`
pipeline). This also fixed a real bug (the gate worklet name was `noiseGate`;
correct is the hyphenated `noise-gate`) and added per-model sample rates
(DTLN 16 kHz, others 48 kHz), context `resume()`, and SIMD wasm selection.
Still needs live §D testing per model, and depends on cinny shipping the
DTLN (`denoise/workadventure/`) + DeepFilterNet (`denoise/deepfilternet/`)
asset trees (it already does).
- **Rebase-fragility (build agent MED):** the `CallViewModel` spotlight override
edits hot upstream lines (renamed `spotlightSpeaker$`→`autoSpotlightSpeaker$`).
For cheaper future rebases, refactor it into a `src/lotus/lotusSpotlight.ts`
wrapper that takes the upstream stream and returns the overridden one, leaving
upstream's definition byte-identical (a single import + two token swaps).
- **Denoise asset coupling (build agent HIGH):** the fork loads `./denoise/*`
shipped by cinny, not by the fork build (documented in the processor). Add an
integration smoke-check that `GET …/element-call/denoise/rnnoise.wasm` == 200,
and pin the `@sapphi-red/web-noise-suppressor` version both repos expect.
- **Unconditional effect registration (build agent LOW):** focus/audio-inject/
quality/decorations register widget handlers on every embedded call (true
no-ops for a non-Lotus host). Intentional; gate behind a coarse `lotus=1` flag
if strict zero-footprint is desired.
- **Privacy (security agent):** decoration/inject URLs accept any `https`; ideally
restrict to the homeserver media origin host-side. Call-state exposes
userId/deviceId/speaking to the (trusted, same-origin) host — documented.
**Nothing here blocks the §D live test — but every feature still needs it.**
### 12.3 Safe rollout when prod is the only test environment
Every Phase-2 feature is now **dormant by default** — with the flags cinny sets
today, the fork behaves identically to the parity build (`#1` was decoupled onto
`lotusDenoiseSource=1` so it no longer collides with the host's `lotusDenoise=ml`
shim). This enables a low-risk incremental rollout even without a staging env:
1. **Ship dormant first.** Publish the `lotus` branch (e.g. `0.20.1-lotus.1`),
bump cinny's pin, deploy. With no Lotus flags set / no Lotus actions sent,
this is upstream-equivalent (only inert, holistically-reviewed code runs).
"Testing" here = confirm a normal call still works.
2. **Enable ONE feature at a time**, each independently revertable:
- URL-flag features (#2 `lotusCallState`, #5 `lotusTransparent`/`lotusTheme`,
#1 `lotusDenoiseSource`): add the flag in `CallEmbed.getWidget`, deploy,
test that one feature, roll back just that flag if needed.
- Action features (#3,#4,#6,#7): wire the host send + (for #2) the
`listenAction` ack, gated on join (§12.1 F1).
3. **#1 denoise cutover is a coordinated 2-step** (do together): set
`lotusDenoiseSource=1` AND remove the `lotusDenoise()` shim injection +
`lotusDenoise=ml` param in cinny — otherwise audio is denoised twice.
Roll back = revert both.
4. Baseline is always upstream-equivalent, so any single feature can be disabled
by flipping its flag/send off without touching the rest.
**Blocker to step 1:** publishing the `lotus` branch needs a Gitea npm token
(the admin token used for the `0.20.1` parity publish was deleted). Either
provide a token for a manual `npm publish`, or stand up the Gitea Actions runner
- `GITEA_NPM_TOKEN` secret so a `v0.20.1-lotus.1` tag auto-publishes.
-159
View File
@@ -1,159 +0,0 @@
# Lotus Chat — Open Bugs & Technical Debt
**Only OPEN and awaiting-verification items live here.** Resolved findings
(fixed-and-verified, false-positives, won't-fix) have been removed to keep this
actionable — the full history is in git. Items fixed in code but not yet
verified in a real environment are in **Needs Verification** below and have
step-by-step checks in [`LOTUS_TESTING.md`](./LOTUS_TESTING.md).
> Design rules for any fix here: follow the **Native-Cinny Law** and **TDS
> Design Law** in [`LOTUS_TODO.md`](./LOTUS_TODO.md).
---
## ⚠️ Needs Verification — fixed in code, awaiting live testing
Implemented and gate-green; confirm each per `LOTUS_TESTING.md`, then delete the row.
| ID | Item | File / area | Test |
| :--- | :------------------------------------------------------------------------------------- | :--------------------------------------------------- | :-------------------------------------------------------------------------------- |
| #2 | Chat-background animation flicker (`contain:paint`) | `lotus/chatBackground.ts` | F1 |
| #4 | Ringtone re-fixes: classic loudness + caller decline notice (A2 ✓ live) | `CallEmbedProvider.tsx`, `ringtones.ts` | A1,A3,A4 |
| #6 | Background vs. seasonal theme mutual exclusion | `state/settings.ts`, `General.tsx` | F2 |
| #7 | Composer toolbar touch targets (≥44px) | `room/RoomInput.tsx` | E1 |
| #8 | Room Settings horizontal overflow (mobile) | `components/page/style.css.ts` | E2 |
| #9 | Modal fullscreen on mobile (`useModalStyle`) | 22+ modal files | E3 |
| #10 | Composer not hidden by keyboard (`100dvh`) | `src/index.css` | E4 |
| #12 | PiP "All muted" badge re-fixed (was firing on any single mute) | `hooks/useCallSpeakers.ts` | G1 |
| N96 | Call-recovery overlay single "Back" button | `call/CallView.tsx` | A7 |
| N95 | AFK-monitor mic released on mute (OS indicator clears) | `hooks/useAfkAutoMute.ts` | L1 |
| N108 | Maskable PWA icons (Android adaptive) | `public/manifest.json` + `res/android/maskable-*` | L2 |
| EC | EC iframe load watchdog + self-heal + recovery UI | `plugins/call/CallEmbed.ts`, `CallView.tsx` | A7 |
| N105 | Notification clicks work after tab close (SW `notificationclick` + `showNotification`) | `sw.ts`, `utils/dom.ts`, `ClientNonUIFeatures.tsx` | get a msg notif, close the tab, click it → app focuses/opens + routes to the room |
| Gal | MediaGallery lazy-decrypt (true virtualization deferred) | `room/MediaGallery.tsx` | H1 |
| a11y | aria-labels: edit-history / reaction / thread / reply | `message/*` (`FallbackContent`, `Reaction`, `Reply`) | I |
**Verified working in live testing (2026-06):** A2, B1B4, C1, C3, D (mic/camera/deafen/screenshare/fullscreen/more-menu/PiP). Denoise quality in D is still poor — tracked under the denoise project, not a regression.
---
## 🧩 Element Call source-level items — now actionable via the fork
> 🔱 **[EC-FORK]** **UPDATE 2026-06-30: Phase 2 IMPLEMENTED.** We own and
> self-build Element Call (`LotusGuild/element-call` →
> `@lotusguild/element-call-embedded@0.20.1-lotus.1`, cinny wired). A5/A6/A7
> below are **fixed in the fork** — they are now ⚠️ awaiting **live
> verification** (`LOTUS_TESTING.md` §D2), not open work. See
> [`HANDOFF_ELEMENT_CALL_FORK.md`](./HANDOFF_ELEMENT_CALL_FORK.md) §10. Delete each
> row once verified live.
The in-call participant grid is rendered **inside EC's app** — now editable source
(previously a prebuilt npm bundle we could only style around). Status of the items
from testing:
- **A5 — "Focus camera": ⚠️ FIXED in fork, awaiting verify (D2-3).** cinny now
sends an `io.lotus.focus_participant` widget action that pins a participant in
EC's layout (coexisting with / overriding the screenshare spotlight); the old
`.click()`-the-tile DOM hack in `CallControl.ts` is deleted.
- **A6 — avatar decorations in-call: ⚠️ FIXED in fork, awaiting verify (D2-4).**
cinny pushes `io.lotus.decorations` (per-user APNG URLs) and the fork renders
them on EC's participant video-tile avatars — not just our pre-join lobby roster.
- **A7 — mic dead after EC's "Reconnect": ⚠️ FIXED in fork, awaiting verify
(D2-1).** Denoise moved into EC's mic-capture/publish pipeline as a first-class
LiveKit `TrackProcessor` (flag `lotusDenoiseSource=1`); EC re-runs it on every
(re)publish, so reconnects keep denoise alive natively. The build-time
`getUserMedia`/`index.html` injection (the root cause) is removed. **Highest
blast radius — everyone's mic; verify D2-1 carefully.**
---
## 🔴 Open — Actionable
### Calls / Audio
- ~~**N127 — ML denoise shim is never injected in `vite dev`.**~~ **RESOLVED (dissolved by the A7 denoise cutover).** `vite.config.js` no longer injects a getUserMedia shim at all — the forked Element Call runs ML denoise in-source as a LiveKit `TrackProcessor` (activated by `lotusDenoiseSource=1`), so there is no build-time injection that could be missing in dev. Nothing to fix.
### 🧨 Encryption / E2EE — ⚠️ EXTREME COMPLEXITY · 🧠 PLANNING SESSION REQUIRED · 👤 SENIOR ENGINEER
> **Observed live in prod 2026-06-30** on `chat.lotusguild.org` during a 2-person
> **Element Call** (E2EE enabled). These span **client rust-crypto (via
> `matrix-js-sdk@41.6.0-rc.0`) ↔ Synapse ↔ Element Call's MatrixRTC E2EE** and are
> very likely **interrelated** (see KE-1 → KE-2). Do **not** spot-fix — they need
> a dedicated cross-system planning session with the homeserver owner. Capture
> full client console + a synapse-side trace for the same call before starting.
> **None of these are caused by the EC fork work** (the issues reproduce on the
> old build; the local mic/denoise path is unrelated to key distribution).
- **KE-1 — One-time-key (OTK) upload conflict storm (CRITICAL, root-cause candidate).**
`POST /_matrix/client/v3/keys/upload` returns `400 M_UNKNOWN: One time key
signed_curve25519:AAAAAAAAAGQ already exists. Old key: {…} new key: {…}`
firing **continuously** (many/sec). The client repeatedly tries to publish an
OTK at a key id the server already holds **with a different value**, i.e. the
rust-crypto key store and Synapse have **diverged OTK state**. Impact: floods
the crypto outgoing-request loop and is the prime suspect for the downstream
missing-key failures (no fresh OTKs ⇒ no new Olm sessions ⇒ undecryptable
to-device key events). _Investigate:_ device/key-store reset-or-restore
mismatch, OTK id-counter desync, RC-SDK (`41.6.0-rc.0`) regression, or a
Synapse OTK bug. Repro signature: grep console for `already exists`.
**Extreme — planning session.**
- **KE-2 — Element Call media keys not arriving/decrypting → audio & video cut out (CRITICAL).**
`MissingKey: missing key at index N for participant @user`, `skipping decryption
due to missing key`, `MissingKey: key set not found for @user at index 0`, and
rust-crypto `WARN … Received an unexpected encrypted to-device event …
event_type="io.element.call.encryption_keys"`. EC distributes per-participant
media keys as **encrypted to-device `io.element.call.encryption_keys`** events;
these aren't being received/decrypted in order, so remote LiveKit audio/video
can't be decrypted — **this is the "friend's audio cuts out occasionally"
symptom.** Almost certainly downstream of **KE-1** (broken Olm sessions). Spans
EC's MatrixRTC E2EE + rust-crypto to-device + Synapse. **Extreme — planning
session.**
- **KE-3 — Timeline decryption error: missing `algorithm` field (HIGH).**
`Error decrypting event (… type=m.room.encrypted …): DecryptionError[msg:
missing field 'algorithm' at line 1 column 138 …]`. A malformed/legacy
encrypted event (or a serialization mismatch in the RC SDK) that rust-crypto
can't parse. Lower frequency than KE-1/2 but a distinct decode-path failure —
capture the offending event id (`$SASBBzoqj…` seen) and inspect its raw content.
- **KE-4 — MatrixRTC delayed-event / membership timeouts (MEDIUM-HIGH, reliability).**
`[MembershipManager] Network local timeout error while sending event, immediate
retry … AbortError: Restart delayed event timed out before the HS responded`,
with repeated `org.matrix.msc4157.update_delayed_event`. MSC4140/4157
delayed-event reliability against `matrix.lotusguild.org` — can cause stale/ghost
call membership and missed leave events. May be partly **homeserver
responsiveness**; correlate with synapse latency/load. Include in the same
planning session since it shares the call-reliability + HS-interaction surface.
### Security & Privacy
- **N97 — Access token stored in plaintext `localStorage`** (`state/sessions.ts`), vulnerable to XSS; device ID likewise. Architectural — needs a token-protection / session-storage redesign.
- **Session writes are non-atomic and not cross-tab synced** (`state/sessions.ts`) — risks inconsistent state / races across tabs.
- **Persisted PII without encryption:** user status message + expiry (`settings/account/Profile.tsx`), unsent composer drafts (`room/RoomInput.tsx`). Leak risk on shared devices.
### PWA / Offline / Notifications
- **N107 — SW has no `push` handler** — Web Push delivery is entirely non-functional. Needs a `push` listener + a Matrix push-gateway integration.
- **No app-asset caching strategy** (`src/sw.ts`) — no offline capability.
- ~~**`manifest: false`** may block PWA install~~ — **verified OK (2026-06):** `index.html` links `/manifest.json`, which exists in `public/` and is copied to `dist/`; VitePWA intentionally doesn't generate one. Not a bug.
### Dependencies & Build
- **`matrix-js-sdk` pinned to a Release Candidate** (`41.6.0-rc.0`); `@atlaskit` and build tools (`vite`, `typescript`, `eslint`) on unstable/experimental pins — review for stable versions; RC SDK is a tree-shaking/bundle-size risk.
- **Build-time overhead:** `lotusDenoise` does heavy sequential `fs` work in `closeBundle`; `viteStaticCopy` config is complex with redundant renames — could be streamlined.
### Code Hygiene / DevEx
- **Automated test suite — 545 tests across 62 modules, a hard CI gate.** `npm test` runs Node's built-in runner via `tsx` (not vitest — Vite 8 is ahead of vitest's range) and **blocks the build job on failure**. Broad pure-logic coverage: utils (common, regex, sanitize/XSS, time, matrix, matrix-uia, mimeTypes, sort, accentColor, findAndReplace, AsyncSearch, ASCIILexicalTable, keyboard, room, matrix-crypto, featureCheck, syntaxHighlight, imageCompression, user-agent, callSounds), state (settings, sessions, recentSearches, upload, typingMembers, lists, room-list, toast, scheduledMessages, backupRestore, callEmbed/callPreferences, spaceRooms, …), plugins (matrix-to, call/utils, via-servers, bad-words, recent-emoji, custom-emoji, markdown block/inline/utils), OIDC (cs-api, useParsedLoginFlows, oidcState), lotus/avatarDecorations, message-search, search filters. Prevention work has caught + fixed **4 real bugs** (`findAndReplace` infinite-loop; `getSettings` crash-on-load when storage is blocked; `isMacOS` never matching modern Macs; `isMLDenoiseSupported` throwing `ReferenceError` instead of returning false on browsers lacking the `AudioWorkletNode` binding). **Next:** component/integration tests (the untestable-under-tsx DOM/React surface).
- **Extensive `as any` casts** across `src/` — gradual typing cleanup.
- **`types/matrix/` mirrors SDK types** instead of importing them — drift risk.
- **Hardcoded CDN URL** should move to an env var (the decoration CDN is now single-sourced in `avatarDecorations.ts`, but the literal is still in-repo).
- **`patch-folds.mjs` edits `node_modules` directly** — consider `patch-package`.
- **Infra docs:** `contrib/nginx` lacks security headers (HSTS/CSP) + uses rewrites over `try_files`; `contrib/caddy` has a placeholder path. CI/CD (`prod-deploy.yml`): sequential deploy, aggressive 1-min Netlify timeout, `package-manager-cache: false`.
- **README:** keep the fork-sync version + logo path current. (`CONTRIBUTING.md` is intentionally left as upstream Cinny's — not a Lotus concern.)
- **Architecture notes (low priority):** deep `features/` + `hooks/` nesting, many small coupled hooks, possible dead CSS/components, `SpacingVariant` / `DropTarget` recipe simplification.
- **Git workflow (forward-looking):** keep commits scoped — past monolithic "fix all bugs" commits and inconsistent prefixes hurt `git bisect`.
### Big Projects
- **#5 — Seasonal themes & chat-background redesign.** Current backgrounds are basic CSS; goal is high-fidelity, research-backed, GPU-accelerated designs (layered `oklch`, `backdrop-filter`, `contain:paint`) with WCAG-AA overlay contrast. Treat each as its own design sprint.
-1199
View File
File diff suppressed because it is too large Load Diff
-582
View File
@@ -1,582 +0,0 @@
# Lotus Chat — Manual Testing Guide
**Generated:** June 2026
**Scope:** Everything landed on the `lotus` branch since the v4.12.3 merge that I (Claude) could **not** verify statically and that needs a human in a real environment to confirm. Work through it top-to-bottom; the highest-risk / hardest-to-reproduce items are first.
> **How to report back:** For each numbered check, tell me **PASS** / **FAIL** (or **partial**). On any FAIL, include: what you saw vs. expected, the browser/OS (and whether web LXC 106 or the desktop/Tauri build), the theme you were on, and any **browser console** errors (F12 → Console). Screenshots help for anything visual.
## Environment notes
- You push from your own machine; these commits are local on `lotus` until you do.
- Test the **web** build (LXC 106 / `code.lotusguild.org`) first; re-run the **call** + **poll** sections on the **desktop (Tauri)** build too, since CSP and the EC iframe behave differently there.
- Several call features need a **second participant** (second account on another device/browser, or a colleague). Items that need this are marked **👥 2 people**.
- A couple of call items need a **third room/call** in parallel — marked **👥👥**.
---
## Commits covered
| Commit | Area |
| :--------- | :--------------------------------------------------------------------------- |
| `caf6318a` | Poll vote buttons → folds tokens (N4) |
| `c67aed01` | In-call incoming-call banner (#4b) |
| `4a875884` | Selectable ringtone (#4a) |
| `0394fce9` | EC iframe load watchdog + recovery UI; avatar decorations on call tiles (#3) |
| `d2946c00` | Upload retry/backoff, presence-on-unload, typed m.direct |
| `b7e1f89c` | Timeline/composer/emoji perf memoization |
| `c0f98672` | Upstream **Element Call 0.20.1** merge (regression sweep) |
---
## A. Calls — new ringtone + notification work (highest priority)
### A1. Ringtone selection — preview in Settings
**Steps**
1. Open **Settings → General**, scroll to the **Calls** section.
2. Find the new **Ringtone** dropdown (just above **Ringtone Volume**).
3. Select each option in turn: **Classic, Chime, Soft, Retro, Silent**.
**Expected**
- Selecting **Classic** plays the existing `call.ogg` clip (cut off after a few seconds).
- **Chime / Soft / Retro** each play a short, distinct synthesized preview.
- **Silent** plays nothing.
- Changing **Ringtone Volume** then re-selecting a ringtone previews at the new volume.
- No console errors.
> ⚠️ **Known browser limitation:** the synthesized tones use WebAudio. If a preview is ever silent, click anywhere on the page once (a "user gesture") and retry — browsers suspend audio until the page has been interacted with. The Settings preview is _after_ a click so it should always sound; this note matters more for A3.
### A2. Ringtone selection persists
1. Set Ringtone to **Retro**, reload the app.
2. **Expected:** the dropdown still shows **Retro** (setting persisted).
3. Bonus: in devtools, set `localStorage.settings` to a bogus `ringtoneId` and reload → it should fall back to **Classic**, not break.
### A3. Incoming call uses the selected ringtone — 👥 2 people
**Setup:** Account A (you) and Account B in a **DM** or a **private (invite-only) group** room.
1. As A, pick a non-silent ringtone (e.g. **Chime**).
2. From B, **start a call** in that DM/room. Do **not** answer on A.
**Expected on A**
- The full-screen **Incoming Call** dialog appears (caller name, room avatar, Answer / Reject).
- The **selected ringtone loops** until you answer/reject/ignore (at the set volume).
- Answer → joins the call. Reject (DM) / Ignore (group) → dialog dismisses and ring stops.
- Set ringtone to **Silent** and repeat → dialog still appears, **no sound**.
### A4. In-call banner for a second incoming call — 👥👥 (the trickiest one)
**Setup:** You (A) already **in a call** in Room 1. Account B can call you in a **different** Room 2 (a DM or private group you share). Ideally a third account C, or B leaves Room 1's call first.
1. While A is **actively in Room 1's call**, trigger an incoming call to A from **Room 2**.
**Expected on A**
- **No** full-screen takeover. Instead a **compact banner appears in the top-right corner** with the caller's avatar, room name, "Incoming voice/video call", and **Answer / Reject (or Ignore)** buttons.
- It plays a **single soft ping**, _not_ a looping ring (so it doesn't talk over your active call).
- The banner does **not** cover your active call's controls/PiP in a way that blocks them.
- **Answer** → switches you into Room 2's call. **Reject/Ignore** → banner disappears.
- The banner auto-dismisses if the caller hangs up / the call times out.
**Also verify the no-op case:** while in Room 1's call, if a notification for **Room 1 itself** arrives, **nothing** should pop up (no banner, no dialog).
### A5. Camera focus during screenshare (#1) — 👥 2 people
**Setup:** You (A) and B in a call; B (or another participant) **sharing their screen**, and at least one person with **camera on**.
1. As A, open the **participant glance** (the stacked avatars / member list for the call) and click a participant who has their **camera on**.
2. In the menu, click **"Focus camera"**.
**Expected**
- The view switches to **spotlight** and **pins that person's camera tile**, overriding the auto-spotlighted screenshare.
- It **stays** on that camera (doesn't immediately snap back to the screenshare).
- If you pick someone with their camera **off**, it should at worst just toggle spotlight (graceful fallback), not error.
### A6. Avatar decorations on call tiles (#3) — 👥 2 people
**Setup:** A participant in the call has an **avatar decoration** set (Settings → Profile decoration).
1. Join a call with that participant.
2. Look at **our** participant roster / prescreen tiles (not the avatars rendered inside the Element Call video grid — those are EC's and out of scope).
**Expected:** the decoration ring/overlay renders around that participant's avatar on the call tile, the same way it does in member lists.
### A7. EC iframe load watchdog + recovery UI (#EC, N96)
This guards against a permanently-stuck "Loading…" call. Also covers the N96 button-label fix (the old "Retry" and "Leave" buttons were identical — now there is a single **"Back"** button).
1. Normal case: **join a call** → it should connect within a few seconds as usual (the watchdog stays invisible).
2. Failure case (best-effort to reproduce): throttle your network hard (devtools → Network → Offline) **right as** you click join, or block the Element Call origin, so the iframe can't finish loading.
**Expected**
- On a genuine failure/timeout (~25s), instead of an endless spinner you get a **visible error overlay with a single "Back" button** (the old "Retry" + "Leave" pair is gone — they did the same thing and "Retry" was misleading).
- Clicking **Back** returns you to the call prescreen, where you can manually click Join to try again.
- Normal joins must **not** trigger the error overlay (no false positives) — this is the important part to confirm.
- **Self-heal:** if the error overlay appears on a slow network but EC then finishes loading anyway, the overlay should **dismiss itself** and drop you into the live call. Worth confirming on a deliberately throttled-but-not-blocked connection.
---
## B. Polls (N4) — render correctly on non-TDS themes
This was the actual bug: poll buttons used undefined CSS variables, so on the **default (non-Lotus-Terminal) themes** they rendered with invisible borders / no selected state.
### B1. Poll renders on a default theme — ✅ PASS
1. Switch to a **default Cinny theme** (Settings → Appearance — **not** Lotus Terminal / TDS). Test both a **dark** and a **light** theme.
2. In any room, create a poll (composer → poll button): a **single-choice** poll with 3 options.
**Expected**
- Each option is a clearly **bordered** button with visible rounded corners.
- A **radio circle** indicator is visible on the left of each option.
- Text, and (after votes) the percentage, are legible.
### B2. Voting + selected/progress state
1. **Vote** on an option.
**Expected**
- The selected option shows a **filled accent border + filled radio**, and an **accent progress-bar fill** grows behind it proportional to the vote %.
- The percentage and total vote count update.
- Click again / pick another option → selection moves correctly (single-choice replaces; the bar redraws).
### B3. Multiple-choice poll
1. Create a poll allowing **multiple selections**.
**Expected**
- Indicators are **square checkboxes** (not circles); selected ones show a **✓** that's legible against the filled box.
- You can select **several** options; each shows its own progress fill.
### B4. Lotus Terminal theme regression — ✅ PASS
1. Switch to **Lotus Terminal / TDS** theme and re-open a poll.
**Expected:** still looks correct (the fix uses theme tokens, so the TDS accent should now drive it) — no worse than before.
---
## C. Robustness / background behavior
### C1. Presence updates on tab close
1. Open the app, then **close the tab** (or quit the browser).
2. From another session/device, check your **presence** shortly after.
**Expected:** you go **offline/away** reliably (the unload now uses `fetch({keepalive})`). Previously this could be missed.
### C2. Upload retry on flaky network (best-effort)
1. In devtools → Network, set a throttle that drops/slows requests, or toggle Offline briefly **during** a file upload.
**Expected**
- A transient failure **retries** (up to 3×, with backoff) and the upload can still succeed once the network recovers.
- A genuine, permanent rejection (e.g. file too large / 4xx) still **fails fast** with the usual error — it should **not** spin retrying.
### C3. General timeline/composer perf (no functional regression)
The memoization changes are invisible if correct. Just confirm **nothing broke**:
- Open a busy room; scrolling, jump-to-latest, mark-as-read all still work.
- Composer: send a message, upload a file, share a location, pick an emoji and a sticker — all still work.
---
## D. Element Call 0.20.1 merge — regression sweep (👥 2 people)
The upstream bump changed EC's internals and DOM selectors; our call controls drive that iframe, so sweep them. In a live call with 2 people, confirm **each** of our control-bar buttons works:
- [ ] **Mic** mute/unmute (icon + actual audio)
- [ ] **Camera** on/off
- [ ] **Deafen / Sound** toggle (your deafen key too)
- [ ] **Screenshare** start/stop (and the "Share your screen?" confirm)
- [ ] **Screenshare audio** mute toggle
- [ ] **Fullscreen** toggle
- [ ] **⋮ More** menu → **Spotlight/Grid**, **Reactions**, **Settings** each open the right EC panel
- [ ] **End** call leaves cleanly
- [ ] **PTT** (push-to-talk) if enabled: hold key = transmit, release = mute; releasing on blur works
- [ ] **AFK auto-mute** if enabled: goes muted after the timeout
- [ ] **PiP** (picture-in-picture) mini window: drag, resize, fullscreen button, return-to-call; the "You muted" / "All muted" badges show on the right person
- [ ] **Denoise** (if ML noise suppression enabled): call audio still flows, no silence
If any control does nothing, that usually means an EC DOM selector changed — capture the console and tell me which button.
---
## D2. Element Call **fork** — Phase 2 feature sweep (👥 2 people) — `0.20.1-lotus.1`
> The whole EC iframe is now our **self-built fork** (`@lotusguild/element-call-embedded@0.20.1-lotus.1`).
> Five features are **active** (the host sets their flags / sends their actions); two ship **dormant**.
> **Confirm you're on the fork first:** EC iframe console prints `Element Call embedded-v0.20.1-lotus.1`
> (the old build prints `embedded-v0.20.1`). If it says the old version, the web deploy hasn't landed —
> the fork features won't be present, so don't test D2 yet.
> For non-dev testers, each item below also states the plain "✅ good if / ❌ tell us if" outcome.
### D2-1. Denoise **in-source** — survives reconnect (fixes A7) ⭐ highest risk (everyone's mic)
Flag: cinny sets `lotusDenoiseSource=1` when ML denoise is selected (the old build-time getUserMedia
shim is **removed**). This is the single change with the widest blast radius — test deliberately.
- [ ] **Audio flows, no silence** with ML denoise on (baseline, also §D line 204).
- [ ] **Reconnect (the A7 fix):** in a call with ML denoise on, kill network ~10 s (devtools → Offline)
so EC shows "Connection lost / Reconnect", then restore. **Mic still works AND still denoised**
afterward, **without** End+rejoin. _(This is the exact bug that was reintroduced then fixed; if it
regresses, mic dies on every reconnect.)_
- [ ] **Mic device switch mid-call** (Settings → change microphone): audio keeps working (same
`restart()` path as reconnect).
- [ ] **Mute → unmute** a few times: audio returns each time.
- [ ] **Each model** if the picker offers them: `rnnoise` (default), `speex`, `dtln`, `deepfilternet`
each loads + denoises, no silence. (All four are in-source now; DTLN runs at 16 kHz, others 48 kHz.)
- [ ] **No double-processing:** audio isn't over-suppressed/artifacted (would mean the old shim is still
injected alongside the in-source engine).
- **Rollback if bad for everyone:** revert the cinny deploy commit (restores the shim + `@element-hq` parity).
### D2-2. Speaking + mute indicators from widget **events** (#2)
Flag: `lotusCallState=1`. cinny now reads speaker/mute state from `io.lotus.call_state` events instead of
scraping EC's DOM (DOM fallback retained). Overlaps **G1**.
- [ ] **Speaking glow** lights the **correct** person when they talk (you, then your friend).
- [ ] **PiP "All muted" / "You muted" badge** points at the right person and updates on mute/unmute.
### D2-3. Focus camera **during a screenshare** (#4 / A5)
Action: cinny sends `io.lotus.focus_participant` (the DOM `.click()` hack is gone). Overlaps **A5 / G2**.
- [ ] Person A screenshares; Person B camera on; **MemberGlance → Focus camera** on B → B's camera is
spotlighted **alongside/over** the shared screen (not ignored).
- [ ] Camera-**off** target = graceful (no error, no kick out of the screenshare).
### D2-4. In-call avatar decorations (#6) — **NEW, beyond A6**
Action: cinny pushes `io.lotus.decorations`. **A6 only covered the lobby roster** and called in-call EC
tiles out of scope — that's now in scope.
- [ ] A participant with a **Profile decoration** joins **camera off** → the decoration ring renders on
their **in-call video-tile avatar** (inside EC, not just the lobby), correctly sized/positioned.
- [ ] Decoration tracks the right person across grid/spotlight layout changes; disappears when they leave.
### D2-5. Native transparent background (#5)
Flag: `lotusTransparent=1` (native, replacing the injected `background:none !important`).
- [ ] Call background looks right — host wallpaper/surface shows through; **no** black box, bad
see-through, or layout breakage (also covered loosely by §D2 "looks right").
### D2-7. In-Call Soundboard (#3 / P5-15) — 👥 2 people — **NEW**
Flag: `lotusAudioInject=1`. A 🔔 **Soundboard** button now sits in the call controls bar (left group,
next to the chat button). Clips are user-uploadable and sync across your devices like emoji packs.
_Prereq:_ Settings → General → Calls → **Soundboard** must be ON (default on).
- [ ] **Upload:** open the soundboard popout → **Upload** → pick a short audio file (mp3/ogg/wav, ≤ 1 MB).
It appears as a clip tile. (Too-big / too-many shows an error, doesn't crash.)
- [ ] **Plays into the call:** with a second person in the call, click a clip. **They hear it**, and
**you hear it locally** too. ✅ good if both hear it; ❌ tell us if only one side does.
- [ ] **Sync:** the uploaded clip shows up on your **other device**/session (account-data sync).
- [ ] **Delete:** the ✕ on a tile removes it (everywhere, after sync).
- [ ] **Off switch:** turn Settings → Calls → **Soundboard** off → the call-bar button disappears.
- [ ] Injecting a clip does **not** mute/interrupt your mic or anyone else's audio.
### D2-8. Call Quality Controls (#7 / P5-31) — 👥 2 people — **NEW**
Action: `io.lotus.set_quality`. User settings in **Settings → General → Calls** (Microphone Bitrate,
Screenshare Bitrate, Screenshare Framerate; all default **Auto**). Admin caps in **Room Settings →
General → Voice → Call Quality Caps**.
- [ ] **No regression at Auto:** with everything on **Auto**, calls/screenshare work exactly as before.
- [ ] **User cap takes effect:** set Microphone Bitrate to **32 kbps**, rejoin/continue a call — audio
still flows (thinner is fine). Set Screenshare Framerate to **15 fps** and share your screen — it
still shares. ❌ tell us if any setting kills audio/screenshare.
- [ ] **Applies mid-call:** changing a setting **during** a call takes effect without End+rejoin.
- [ ] **Room-admin cap (admin needed):** as a room admin, set **Max Microphone Bitrate = 64 kbps** in
Room Settings → Voice. A member whose user setting is higher (e.g. 256) should be **clamped to 64**
(best-effort/UX — this is client-side; hard server enforcement is a separate follow-up).
- [ ] Resetting a setting back to **Auto** removes the cap for the rest of the call.
> Soundboard + quality are no longer "dormant" — if either does nothing, grab the **EC iframe console**
> and check for `io.lotus.inject_audio` / `io.lotus.set_quality` rejections.
### D2-9. Call Permissions — HARD server-side, cross-client (👥 2 people, admin) — **NEW**
This is enforced by the `voice-limit-guard` on the server (re-signs the LiveKit JWT), so it applies to
**every** client, not just Lotus Chat. Set in **Room Settings → General → Voice → Call Permissions**.
_(Requires the guard deployed on LXC 151 — auto-deploys on a `matrix` repo push.)_
- [ ] **Disable screenshare:** as admin, turn **Allow Screen Sharing** off. In a call, the
**screenshare button disappears** in Lotus Chat. ✅ good if no one can screenshare.
- [ ] **Cross-client (the important one):** have someone join the **same room from stock Element / Element
X** and try to screenshare → the server **refuses** the track (it won't publish). This proves it's
not just our client hiding a button.
- [ ] **Audio-only room:** turn **Allow Camera** off too → the camera button disappears and cameras are
server-blocked for all clients; **microphones still work**.
- [ ] **⭐ Live kill (mid-call):** while someone is **actively screensharing**, an admin turns **Allow
Screen Sharing** off. Within a few seconds their screenshare should **stop for everyone** on its own
(no rejoin needed) — this is the server reconcile loop revoking it live. Works even if the sharer is
on stock Element. ✅ good if the share drops within ~35 s; ❌ tell us if it keeps going.
- [ ] **Turning it back on** restores the ability to screenshare/camera (start a new share).
- [ ] **No policy = no change:** a room with Call Permissions left on defaults behaves exactly as before.
> If any D2 item fails, grab the **EC iframe console** (right-click the call → inspect the iframe) — a
> widget-action/payload mismatch shows up there as a `io.lotus.*` rejection or a `MissingKey`/transport log.
---
# Backlog of previously-fixed-but-unverified items
> Sections AD above are **this session's** work. Everything below was fixed in earlier waves and is still flagged **⚠️ UNTESTED** in `LOTUS_BUGS.md` / `LOTUS_TODO.md`. They're grouped by what kind of environment you need (mobile, desktop, screen reader, etc.) so you can knock out a whole category at once. None of these are urgent the way AD are; do them as you have the right device handy.
## E. Mobile / responsive (needs a real phone, or devtools device emulation)
### E1. Composer toolbar touch targets (#7)
On a phone, open a room and the composer toolbar. Tap each button (attach, format, sticker, emoji, GIF, location, poll, schedule, send).
**Expected:** every button is comfortably tappable (≥44×44px), no mis-taps hitting the wrong icon.
### E2. Room Settings — no horizontal overflow (#8)
On a narrow phone screen, open **Room Settings**.
**Expected:** the settings nav panel fills the full width; **no** horizontal scrollbar / sideways scrolling anywhere in the panel.
### E3. Modals go fullscreen on mobile (#9)
On a phone, open several dialogs: Leave Room, Create Room, Create Space, Invite User, Report (room/user/message), Edit History, Forward Message, Remind Me, Schedule Message, Device Verification, Poll Creator.
**Expected:** each opens **fullscreen** (no floating box, no rounded corners / max-width margins). On desktop the same modals should still be the normal centered boxes.
### E4. Composer not hidden by the keyboard (#10) — iOS Safari especially
On a phone (priority: **iOS Safari**), tap into the composer so the on-screen keyboard appears.
**Expected:** the composer input stays **visible above** the keyboard; the layout shrinks rather than the composer sliding under the keyboard.
### E5. Mobile "Saved Messages" access (Mobile Bookmarks)
On a phone, **inside a room**, open the room header **··· More Options** menu.
**Expected:** a **"Saved Messages"** item is present; tapping it opens the bookmarks panel. (This was the only in-room access point missing on mobile.)
---
## F. Visual / theming
### F1. Animated chat background — no flicker (#2)
Settings → set an **animated** chat background (e.g. anim-rain / anim-aurora / anim-stars). Watch the message text and composer while it animates.
**Expected:** smooth animation, **no flickering / shimmering** on message text or the composer, especially after scrolling. Note your GPU/browser if you see artifacts.
### F2. Background vs. Seasonal theme are mutually exclusive (#6)
In Settings → Appearance:
1. Pick a **chat background** → confirm any **seasonal theme** auto-switches off.
2. Pick a **seasonal theme** → confirm the **chat background** auto-clears to none.
3. (Edge) If you have old data with both set, after reload only one should visibly apply (no double-overlay clutter).
### F3. Background / seasonal picker grid layout (N81)
In Settings → Appearance, look at the **Chat Background** and **Seasonal Theme** swatch grids; resize the window narrow→wide.
**Expected:** swatches reflow to fill each row evenly (responsive grid), with no lopsided/orphaned last row at any width.
---
## G. Calls — additional unverified (👥 2 people)
### G1. PiP mute badges point at the right person (#12)
In a call with at least one other person, pop out the **Picture-in-Picture** mini window.
- **You** mute your own mic → a **"You"/muted badge appears bottom-left** (your status).
- A **remote** participant (or all of them) mutes → an **"All muted"** badge appears **top-right** (clearly about other people).
**Expected:** the bottom-left badge is **never** triggered by someone else muting — that was the original bug (it looked like your own mic was muted when it wasn't).
### G2. Full-screen camera broadcasts
1. In a **camera-only** call (no screenshare), confirm the **Fullscreen** button is available (previously only showed during screenshare).
2. Use **MemberGlance → Focus camera** to full-screen/spotlight a specific person's camera. (Overlaps **A5**; if you've done A5 you can skip.)
### G3. PTT badge renders on all themes (N53)
Enable **Push-to-talk** (Settings → Calls) and join a call. Hold the PTT key.
**Expected:** the floating PTT badge above the controls shows "PTT — Hold KEY" when idle and "● Live" (green) while held — on **both** a default theme and Lotus Terminal (it's now a single folds Chip; the old terminal-only variant was removed).
---
## H. Media / performance (needs a room with many images)
### H1. Lazy image decryption (P5-5 / MediaGallery)
Open a room / media gallery with **many images** (ideally encrypted). Scroll down through them.
**Expected:** images decrypt/load as they **approach the viewport**, not all at once on open; scrolling stays smooth and memory doesn't balloon. Off-screen images shouldn't all decode up front.
### H2. Thumbnail framing (P5-6)
Look at **tall portrait** images in the timeline and in the media gallery.
**Expected:** thumbnails are framed **center-top** (so faces/subjects at the top aren't cropped out); no awkward stretching. Opening the full-size viewer still shows the **whole** image (contain, not cropped).
---
## I. Accessibility (needs a screen reader: VoiceOver / NVDA / TalkBack)
With a screen reader on, navigate message hover-actions and content and confirm each control **announces a meaningful label** (not "button" / blank):
- [ ] **Reaction** buttons announce the emoji + count (e.g. "thumbsup reaction, 3 people").
- [ ] **Edit history** button announces "View edit history".
- [ ] **Thread indicator** announces "View thread".
- [ ] **Reply** (jump to original) announces "Jump to original message".
---
## J. Desktop / Tauri build only
### J1. Proactive update notifications (P5-40)
In the **desktop (Tauri)** build, with an update available, launch the app (and/or leave it running ~12h).
**Expected:** an in-app toast/badge alerts you that an update is available, without manually checking Settings. (Needs an actual newer release to point at.)
### J2. DTLN noise suppression sanity
In Settings → Calls, enable **ML noise suppression** with the **DTLN** model, then join a call.
**Expected:** your mic audio still flows (no silence/robotic dropouts) and background noise is reduced. Confirmed working earlier but flagged for a final real-call check; verify on **both** web and desktop.
---
## K. Features — end-to-end unverified
### K1. Remind Me Later
On a message, **··· → Remind Me**, pick a short preset (the 20-min one, or wait one out).
**Expected:** when due, a Lotus toast fires linking to that message; the reminder then clears itself. Survives a reload while pending (stored in account data).
### K2. Advanced search filters (P4-9)
In message search: use the **sender picker** (instead of typing `from:@user`), the **date-range** quick presets (Today / Last week / Last month / Last year), and the **Has link** toggle.
**Expected:** each narrows results correctly and reflects in the search.
### K3. Notification content + click target (P5-20 partial)
Trigger a desktop/browser notification for a new message.
**Expected:** it shows the **real message body** (`username: message`, not "New inbox notification from…"); **clicking it** brings the window to front and navigates **directly to that message** (not just the inbox).
---
## L. Fixed — verify
### L1. AFK auto-mute releases the OS microphone indicator on mute (N95) — 👥 live call
**Context (now FIXED):** `useAfkAutoMute.ts` opened its own `getUserMedia` level-monitor capture for the whole call, so the OS recording indicator (green dot on macOS, mic icon on Windows/Linux) stayed lit even when muted. The capture is now gated on the reactive mic-on state — it runs only while unmuted, so muting releases the stream.
**To verify:**
1. Enable **AFK auto-mute** in Settings → Calls and **join a call**.
2. Manually **mute your mic** using the call controls → the **OS recording indicator should clear** within ~a second.
3. **Unmute** → the indicator should re-appear (capture re-acquired).
4. Also confirm AFK still works end-to-end: stay unmuted and silent past the configured timeout → mic auto-mutes with the "muted after inactivity" toast, and the indicator clears.
### L2. Maskable PWA icon (N108) — Android install
1. On **Android Chrome**, install Lotus Chat as a PWA (Add to Home Screen).
2. Look at the **home-screen icon**.
**Expected:** the icon fills the adaptive-icon shape cleanly (the logo centered with safe-zone padding on the dark background), **not** clipped at the corners or floating in an odd box. Also worth a quick check in Chrome DevTools → Application → Manifest that the two `purpose: maskable` icons load without a 404 (this also validates the manifest's icon paths resolve in production — a pre-existing path convention I couldn't verify statically).
---
## M. New features (this round)
### M1. Search: `has:image` / `has:file` / `has:video` filters
1. Open message search (in a room with shared images/files/videos in history).
2. Run a broad search, then toggle the **Images**, **Files**, **Video** chips (in the filter bar, next to "Has link").
**Expected:**
- Each chip narrows the visible results to that message type; multiple active chips = union (any of them).
- Toggling them off restores the full results. The existing room/sender/date/has-link filters still work alongside.
- **Known limitation (by design):** filtering is client-side over already-fetched results, so the visible count can be lower than the server's total for that query — paginating/loading more pulls in more to filter. Confirm this reads acceptably.
### M2. Search: recent searches
1. Run a few different searches, then **clear the search box** and focus it.
**Expected:** your last (up to 10) distinct searches appear as clickable chips; clicking one re-runs it. A **Clear** affordance wipes the list. The list **persists across a page refresh** (localStorage).
### M3. Custom accent color (non-TDS themes) — ⚠️ needs your visual judgment
1. Make sure **Lotus Terminal (TDS)** is **off**. Settings → Appearance → **Custom Accent Color** → pick a color.
**Expected:**
- The app's accent (buttons, selected/active states, links, primary chips) recolors to your choice **live**.
- **Look critically at quality** (this is the part I can't verify): button **text legibility** (OnMain contrast) on the accent buttons; **hover/active** shades; and **selected-row / chip** backgrounds (the translucent "Container" tints). Try a **light** color and a **dark** color and a **saturated** one.
- If a dark accent makes selected-row text (OnContainer) hard to read, tell me — that's the one spot in the auto-derived palette most likely to need tuning.
- **Reset** clears it back to the theme default.
- Turn **Lotus Terminal ON** → the custom accent should be **ignored** (TDS fixed palette wins) and the picker shows a "non-TDS only" note; turn it back off → custom accent returns.
- Reload → the chosen accent **persists**.
---
### M4. Search: "Pinned only" filter
In message search, toggle the **Pinned** chip.
**Expected:** results narrow to messages currently pinned in their room; composes with the Images/Files/Video chips and room/sender/date filters; toggling off restores results. It also narrows the **encrypted/local-cache** results section (not just server results). Needs a room with actually pinned messages.
### M5. New theme presets (Cyberpunk / Ocean / Blood Red / Classic Matrix / Midnight) — ⚠️ visual judgment
Settings → Appearance → theme picker → try each of the 5 new themes.
**Expected:** each applies a complete, legible dark palette. Code review computed WCAG contrast and all pass AA, but **eyeball these specifically**: **Midnight** (lowest-contrast accent `#6b7ca8` — selected/focus states), **Classic Matrix** (green accents, light-green body text on near-black), **Blood Red** (white-ish text on bright-red buttons). Confirm Success/Warning/Critical (save/leave/delete) still look correctly green/amber/red, not recolored. Switching back to a stock theme should fully revert.
---
## N. OIDC / Next-Gen Auth login (MSC3861) — P4-6
The Lotus client can now sign into OIDC-native homeservers (ones that delegate
auth to a Matrix Authentication Service / MAS), e.g. mozilla.org. lotusguild's
own server is **not** MSC3861, so test EITHER against a **local MAS dev loop**
(full setup in `dev/oidc-test/README.md` — docker-compose + Synapse `msc3861`
delta + a `config.json` override) OR against **mozilla.org** with a real account.
### N1. OIDC login flow (the core test) — needs a MAS homeserver
1. On the login screen, select the OIDC homeserver (local `localhost:8008`, or `mozilla.org`).
2. **Expected:** instead of the username/password form, a single **"Continue with single sign-on"** button appears (password + legacy-SSO are suppressed for that server).
3. Click it → redirected to the provider's login page (MAS / `chat.mozilla.org`).
4. Authenticate there → redirected back to `…/auth/oidc/callback` → a brief "Signing you in…" spinner → you land in the app, logged in.
**Expected:** no console CSP violations; you reach the room list as the OIDC user.
### N2. Session persists across reload (token storage)
After N1, hard-refresh the page.
**Expected:** you stay logged in — the OIDC session (access + refresh token + issuer/clientId/claims) was persisted (`cinny_refresh_token`, `cinny_oidc_*` keys in localStorage).
### N3. Token refresh (long-lived session)
Leave the session past the access-token lifetime (MAS default is short — or revoke the access token in the MAS admin UI to force a 401).
**Expected:** the client refreshes transparently (no logout); the stored access token rotates (reactive 401 refresh via the wired `OidcTokenRefresher`).
### N4. Logout revokes at the issuer
Log out from Settings.
**Expected:** back to login; OIDC tokens are revoked at the issuer's `revocation_endpoint` (best-effort) and all `cinny_*` / `cinny_oidc_*` keys are cleared. Logging back in works.
### N5. Account-management deep-link
Settings → Account.
**Expected:** on an OIDC server a **"Manage account"** card appears (opens the provider's account page in a new tab). On a non-OIDC server (lotusguild) the card is **absent**.
### N6. Non-OIDC regression — password login unchanged
Log into **matrix.lotusguild.org** (password) and **matrix.org**.
**Expected:** identical to before — username/password form (+ SSO button where offered). The OIDC path only activates when discovery advertises an issuer, so nothing changes for these servers.
---
## Priority if you're short on time
1. **A4** (in-call banner) + **A3** (ringtone) — newest, most logic, hardest to reproduce.
2. **B1B3** (polls on a default theme) — the confirmed visual bug.
3. **D** (EC 0.20.1 control sweep) — guards against the upstream merge breaking calls.
4. **A7** false-positive check (normal joins don't show the error overlay).
5. Everything else.
-728
View File
@@ -1,728 +0,0 @@
# Lotus Chat — Work Backlog
**Repo:** `lotus` branch at `https://code.lotusguild.org/LotusGuild/cinny`
**Deploy:** push to `lotus` → CI → auto-deploy to `chat.lotusguild.org` (~11 min)
---
## ⚠️ TDS DESIGN LAW — READ BEFORE TOUCHING ANY UI
> **ALL Lotus Terminal Design System (TDS) styling — colors, animations, glows, borders, fonts, spacing — MUST come exclusively from `/root/code/web_template/base.css` CSS variables.**
> Do NOT hardcode hex values. Do NOT invent new variable names. Do NOT deviate from the design tokens defined in that file.
> The canonical variable reference: `--lt-accent-orange`, `--lt-accent-cyan`, `--lt-accent-green`, `--lt-glow-orange`, `--lt-box-glow-*`, `--lt-border-color`, etc.
> Reference implementation for code patterns: `/root/code/tinker_tickets/` (markdown.js, base.js, ticket.css)
> This rule applies to EVERY task in this file without exception.
---
## 🧩 NATIVE-CINNY LAW — EVERY FEATURE MUST FEEL LIKE STOCK CINNY
> **Every feature we implement must feel native to the upstream Cinny app — indistinguishable from something the Cinny team would have shipped.** Reference: <https://github.com/cinnyapp/cinny>.
>
> Concretely this means:
>
> - **Use the `folds` design system, not bespoke UI.** Build with folds primitives (`Button`, `Chip`, `IconButton`, `Menu`, `MenuItem`, `Dialog`, `Modal`, `Input`, `Switch`, `Badge`, `SettingTile`, `SequenceCard`, etc.) and folds tokens (`color.*`, `config.space.*`, `config.radii.*`, `config.borderWidth.*`). No hardcoded hex/`rgba()` for UI chrome, no invented/undefined CSS variables.
> - **Match Cinny's existing patterns.** Before adding UI, find the closest existing Cinny component/flow and mirror it (e.g. a new dropdown uses `Button`+`PopOut`+`Menu`+`MenuItem` like the rest; a new modal has a `Header` with a close `IconButton`; a new setting is a `SettingTile` inside a `SequenceCard`). Consistency with stock Cinny beats personal style.
> - **Lotus-custom additions should be unobtrusive** and fit Cinny's visual language, spacing, and interaction conventions — a stranger using Cinny should not be able to tell which features are ours.
>
> **The ONE exception:** explicit **Lotus Terminal Design System (TDS)** features, which intentionally have their own distinct look and follow the **TDS Design Law** above. TDS styling is opt-in (only active in Lotus Terminal mode); everything else must look and feel like native Cinny.
---
Completed features are documented in [LOTUS_FEATURES.md](./LOTUS_FEATURES.md).
---
## ✅ Done — Awaiting Verification
Built and gate-green; verify per [LOTUS_TESTING.md](./LOTUS_TESTING.md), then they graduate to LOTUS_FEATURES.md. (Bug-side fixes awaiting verification live in LOTUS_BUGS.md.)
| Feature | Test guide |
| :-------------------------------------------------------------------------------- | :---------------- |
| Full-Screen Camera Broadcasts (per-participant focus) | A5 / G2 |
| Advanced search filters (sender/date/has-link/has:image·file·video/pinned/recent) | K2 / M1 / M2 / M4 |
| Custom Accent Color Picker (non-TDS themes) | M3 |
| 5 Color Theme Presets (Cyberpunk/Ocean/Blood Red/Matrix/Midnight) | M5 |
| Intersection-based lazy media loading | H1 |
| Context-aware thumbnail previews | H2 |
| Desktop — proactive update notifications (Tauri) | J1 |
| Remind Me Later | K1 |
| Mobile Bookmarks access | E5 |
| In-Call Soundboard (P5-15, uploadable clips → real call inject) | D2-7 |
| Call Quality Controls (P5-31, user + room-admin caps) | D2-8 |
| Call Permissions (P5-31, hard server-side screenshare/camera policy) | D2-9 |
---
Legend:
- `[AUDIT REQUIRED]` — at least one assumption needs code/server verification before implementing
- `[SERVER CHECK]` — depends on a Synapse feature or MSC; verify on `matrix.lotusguild.org`
- `[LOW PRIORITY]` — implement after all higher-priority items
- `[EXTREME COMPLEXITY]` — multi-sprint, plan separately before touching
- `[BLOCKED]` — cannot build until a server upgrade, upstream MSC, or dependency resolves
- `[IMPROVE]` — feature exists in upstream Cinny; this task enhances it for Lotus Chat
Status: `[ ]` pending · `[~]` in progress · `[x]` completed
---
## Server Capabilities (as of June 2026)
- **Homeserver:** `matrix.lotusguild.org`
- **Synapse version:** `1.155.0` (2026-06-18) — fully up to date; last version for Debian 12 (LXC 151 already on Debian 13 Trixie)
- **Matrix spec:** up to `v1.12` formally; newer MSC features via `unstable_features`
### Confirmed facts
| Finding | Impact |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| **MSC flags ON:** `msc4140` · `msc3771` · `msc3440.stable` · `msc4133.stable` · `simplified_msc3575` · `msc4222` · `msc3266` · `msc3401_matrix_rtc` | All safe to use now |
| **MSC flags OFF:** `msc4306` (thread subscriptions) · `msc3882` · `msc3912` · `msc4155` | These features are BLOCKED |
| **MSC3266** room summary: flag `msc3266_enabled: true` set but `GET /v1/rooms/{id}/summary` still returns 404 (M_UNRECOGNIZED) | Room Preview BLOCKED — endpoint not implemented in Synapse 1.155 |
| **MSC3892** relation redaction: not in flags | Reaction Redaction feature BLOCKED |
| **MSC4260** report user: `POST /_matrix/client/v3/users/{userId}/report` returns **200** ✅ | **Report User UNBLOCKED** — endpoint live since Synapse 1.133; ready to build |
| **MSC4151** report room: HTTP 405 on GET = endpoint exists (POST only) | Report Room live ✅ |
| `folds AvatarImage` does NOT accept children | Add frame/overlay inside `UserAvatar.tsx` itself — optional `frameName` prop |
| No in-app toast system exists (was) | Built `ToastProvider` + Jotai queue; at `App.tsx:65` |
| `useUnverifiedDeviceCount()` hook exists | `src/app/hooks/useDeviceVerificationStatus.ts:65-106` |
| Voice player: `AudioContent.tsx:44-223` | Playback rate on hidden `<audio>` at line 217 |
| `CallControl.setMicrophone(bool)` at `CallControl.ts:206-212` | For AFK auto-mute |
| `CallControl.toggleSound()` at `CallControl.ts:230-251` | Push-to-deafen — just wire a hotkey to this |
| matrix-js-sdk has NO arbitrary profile field methods | Use `mx.http.authedRequest()` for MSC4133 |
| Sanitizer (`sanitize.ts`) allows table, div, span, a, code, hr | LFG HTML card is safe locally; test on Element/FluffyChat |
| Sanitizer STRIPS `<math>`/MathML tags | Math/LaTeX task must also modify sanitizer |
| Service worker EXISTS at `src/sw.ts` | Quick-reply task: add `notificationclick` handler |
| `knockSupported()` utility exists at `matrix.ts:376-391` | Knock UX: only need "Request to Join" in `RoomIntro.tsx` |
| `KeywordMessages.tsx` already has custom keyword push rules | Full push rule editor: only non-keyword rule types need new UI |
| `getMatrixToRoom()` in `matrix-to.ts` generates invite URLs | Invite link: just add QR code to room settings |
| ~~Cindy CANNOT inject audio into EC call stream~~ **UNBLOCKED by EC fork**`io.lotus.inject_audio` widget action publishes a clip as a real call track | In-call soundboard CAN now mix into the call (no longer local-only); needs cinny UI to drive the action |
| Folds uses vanilla-extract in non-TDS, NOT CSS custom properties | Custom accent color: must create new vanilla-extract theme variant dynamically |
| Theme presets need ~50 CSS custom properties each | Significant design work before coding |
| `useCallSpeakers.ts` CSS MutationObserver polling | Visual speaking indicator: TDS ring animation on top of existing data |
| MSC3489/3672 live location: BOTH false on server | Live Location BLOCKED |
---
## Key File Reference
| What you need | File | Lines |
| -------------------------------- | ------------------------------------------------------------- | ------------------- |
| Global keydown hook | `src/app/hooks/useKeyDown.ts` | whole file |
| Room navigation | `src/app/hooks/useRoomNavigate.ts` | 19-72 |
| All room IDs atom | `src/app/state/room-list/roomList.ts` | `allRoomsAtom` |
| Room unread counts | `src/app/state/room/roomToUnread.ts` | `roomToUnreadAtom` |
| Overlay portal provider | `src/app/pages/App.tsx` | 65 |
| Portal container div | `index.html` | 101 |
| Room settings tabs | `src/app/features/room-settings/RoomSettings.tsx` | 27-56 |
| State event read/write pattern | `src/app/features/common-settings/general/RoomEncryption.tsx` | 42-52 |
| Power level checker | `src/app/hooks/usePowerLevels.ts` | whole file |
| Slash command registration | `src/app/hooks/useCommands.ts` | 140-537 |
| Chat background picker | `src/app/features/settings/general/General.tsx` | 945-981 |
| Chat backgrounds definition | `src/app/features/lotus/chatBackground.ts` | whole file |
| Matrix.to URL builder | `src/app/plugins/matrix-to.ts` | `getMatrixToRoom()` |
| Media event content types | `src/app/types/matrix/common.ts` | 46-91 |
| Media URL conversion | `src/app/utils/matrix.ts` | `mxcUrlToHttp()` |
| Message pagination (search) | `src/app/features/message-search/useMessageSearch.ts` | 74-121 |
| Infinite pagination pattern | `src/app/features/message-search/MessageSearch.tsx` | 234-365 |
| Poll event format | `src/app/components/message/content/PollContent.tsx` | 1-320 |
| Theme class application | `src/app/hooks/useTheme.ts` | 25-60 |
| Animations file | `src/app/styles/Animations.css.ts` | whole file |
| Message status (EventStatus) | `src/app/features/room/message/Message.tsx` | 84-142 |
| Call member change events | `src/app/hooks/useCall.ts` | 37-52 |
| Mic control in calls | `src/app/plugins/call/CallControl.ts` | 206-212 |
| Device verification hook | `src/app/hooks/useDeviceVerificationStatus.ts` | 65-106 |
| Knock room support check | `src/app/utils/matrix.ts` | 376-391 |
| Room join button location | `src/app/components/room-intro/RoomIntro.tsx` | 25-119 |
| Notification mute via push rules | `src/app/hooks/useRoomsNotificationPreferences.ts` | 110-150 |
| Message text body CSS | `src/app/components/message/layout/layout.css.ts` | 182-205 |
---
## Priority 3 — Higher complexity / lower daily frequency
### [ ] P3-4 · Accessibility Improvements (WCAG 2.1 AA)
**What:** Comprehensive audit and fix pass targeting the critical user paths:
- Room list navigation (keyboard-only)
- Reading messages in the timeline (screen reader announces new messages)
- Composing and sending a reply
- Opening and closing modals (focus trap, return focus)
- ARIA labels on all icon-only buttons
**Scope:** Do NOT attempt to make every corner of the app AA-compliant in one pass — focus on the golden path (open app → find room → read → reply → send).
**[AUDIT REQUIRED]** — Run an automated audit first: `npx axe-core` or browser DevTools accessibility tree. Document every violation before writing a single line of code. Prioritize by severity (critical > serious > moderate).
**Investigation Findings:**
- **Root Cause:** Inconsistent focus management, missing `aria-live` regions for dynamic timeline updates, and sparse global keyboard shortcuts.
- **Approach:** Standardize `focus-trap-react` usage (reference `RoomNavItem.tsx`). Add `aria-live` regions to the timeline. Expand `useKeyDown.ts` for section navigation shortcuts.
- **Complexity:** Medium-High (audit is the main work).
---
### [ ] P3-8 · Thread Panel (full side drawer)
**⚠️ LARGEST FEATURE — requires its own planning session before implementation.**
**What:** A right-side drawer for threaded conversations. Currently "Reply in Thread" exists but there is no panel to read or write thread replies.
Features:
- Click "Reply in Thread" → opens thread drawer on the right
- Thread root event shown at the top of the panel
- Full message rendering for all in-thread replies (reuse timeline components)
- Reply input at the bottom (full composer with formatting, emoji, etc.)
- Unread count badge on the thread button in the main timeline
- Keyboard shortcut to close thread panel
**Architecture:**
- New Jotai atom: `activeThreadEventId: string | null`
- New component: `src/app/features/room/thread/ThreadPanel.tsx`
- Rendered alongside `RoomView` as a conditional right panel (mirror the members drawer pattern)
- Filter events in timeline to `m.thread` relation for the active root event ID
- Shares the same `mx` client and room reference as the main timeline
**[AUDIT REQUIRED]** — Deeply audit how `m.thread` relation events are currently stored and retrieved in the matrix-js-sdk. Understand the thread aggregation API: `GET /rooms/{roomId}/relations/{eventId}/m.thread`. Check if `RoomTimeline.tsx` currently filters out thread replies from the main timeline (it should — confirm).
**Investigation Findings:**
- **Root Cause:** Current `m.thread` events are treated as standard `m.room.message` events and rendered in the main timeline.
- **Approach:** Introduce new Jotai atom `activeThreadEventId`. Create `ThreadPanel.tsx`. Update `RoomTimeline.tsx` to filter out thread relations (`m.relates_to`). Implement aggregation fetch using `GET /rooms/{roomId}/relations/{eventId}/m.thread`. Use `thread.timelineSet` directly for the most accurate thread view.
- **Complexity:** High.
---
## Priority 4 — Specialized, high complexity, or low priority
### [ ] P4-7 · Virtualized Infinite Scroll for Search Results
**What:** Replace the manual "load more" button with an automated, virtualized infinite scroll for search results.
**Approach:** Utilize `@tanstack/react-virtual` in `MessageSearch.tsx` to handle the `nextToken` automatically as the user scrolls.
### [ ] P4-8 · Encrypted Message Search Indexing & Caching
**What:** Implement a persistent local cache for search results, optimized for encrypted rooms.
**Approach:** Use `IndexedDB` to store search metadata (event IDs, timestamps) to prevent redundant server-side decryption/fetching.
### [ ] P4-1 · Thread Notification Mode Per-Thread (MSC3771)
**Spec:** MSC3771 (stable). Depends on Thread Panel (#P3-8).
**What:** Per-thread notification toggle: "All messages" vs "Mentions only". Accessible from the thread panel header. Tracks unread counts separately per thread.
**[AUDIT REQUIRED]** — Implement after Thread Panel. Requires understanding how the SDK tracks per-thread unread counts.
**Complexity:** Medium (after thread panel exists).
---
### [ ] P4-2 · Thread Subscriptions (MSC4306) [BLOCKED]
**Spec:** MSC4306 (Synapse experimental). Depends on Thread Panel (#P3-8).
**What:** "Follow thread" button to receive notifications for a thread you haven't posted in. Uses MSC4306 subscription endpoint.
**[SERVER CHECK]** — `org.matrix.msc4306 = false` on `matrix.lotusguild.org` — BLOCKED until server enables it.
**Complexity:** Medium (after thread panel exists).
---
### [ ] P4-4 · Math / LaTeX Rendering in Messages (LOW PRIORITY)
**Spec:** CS-API §11.5 (stable) — `formatted_body` can contain LaTeX.
**What:** Render `$...$` or `$$...$$` LaTeX expressions in message bodies. Use KaTeX (lightweight, ~100KB, renders server-side-compatible CSS). Must gracefully fall back to raw LaTeX text if KaTeX fails.
**Note:** This is LOW PRIORITY — only useful for academic/technical communities. Implement last.
**[AUDIT REQUIRED]** — Confirm KaTeX bundle size impact on the Vite bundle. Check if matrix-js-sdk's HTML sanitizer strips LaTeX before it reaches the renderer. The formatted_body sanitization pipeline is the main risk here. (Confirmed: sanitizer STRIPS `<math>` tags — must be patched alongside the renderer.)
**Complexity:** Low-Medium.
---
### [ ] P4-5 · Live Location Sharing (MSC3489 + MSC3672) (LOW PRIORITY, HIGH COMPLEXITY) [BLOCKED]
**Spec:** MSC3489 + MSC3672. Implemented in Element Web.
**Note:** Static location sharing is already implemented. This adds live/real-time GPS beacons. Very low priority per user preference.
**What:** Start sharing live location → creates `m.beacon_info` state event → client posts `m.beacon` events on a timer → other users see your position update live on a map.
**[SERVER CHECK]** — `org.matrix.msc3489 = false` AND `org.matrix.msc3672 = false` on `matrix.lotusguild.org` — BLOCKED.
**Complexity:** High. Requires background geolocation API + live map rendering.
---
### [~] P4-6 · OIDC / SSO Next-Gen Auth (MSC3861) — CLIENT-SIDE BUILT, awaiting live verification
**Spec:** MSC3861 / MSC2965, Matrix spec v1.15. OAuth2-native auth via a Matrix Authentication Service (MAS).
**Scope decision (2026-06):** CLIENT-ONLY. We implemented OIDC login _in the Lotus client_ so it can sign into next-gen homeservers (mozilla.org, eventually matrix.org). We deliberately did **not** convert lotusguild's own Synapse to MAS (no account migration; lotusguild keeps password + legacy Authelia SSO).
**Built (matrix-js-sdk already ships the OIDC API; this was wiring):**
- Discovery: `cs-api.ts` `getOidcIssuer()` (stable `m.authentication` + msc2965). Flow hint: `useParsedLoginFlows` `getOidcCompatibilityFlag()` (MSC3824).
- Login: `pages/auth/oidc/{oidcConfig,oidcLoginUtil,oidcState}.ts` (dynamic registration + cache, PKCE authorize), `login/OidcLogin.tsx`, issuer-gated `Login.tsx`.
- Callback: `oidc/OidcCallback.tsx` + `App.tsx` short-circuit (non-hash redirect path).
- Session/refresh: `state/sessions.ts` OIDC fields, `client/{oidcTokenRefresher,oidcLogout}.ts`, `initMatrix.ts` wiring.
- Account mgmt: `settings/account/OidcManageAccount.tsx`.
- 13 unit tests (discovery/flow/session/cache/callback parsing). All gates green.
**Awaiting verification (needs a real MSC3861 server — lotusguild is NOT one):** deploy + log into **mozilla.org** (requires adding mozilla to the deployed `config.json` homeserverList + its domains to the CSP `connect-src`/`img-src` — see below), OR run a local `matrix-authentication-service` + Synapse `msc3861` dev loop.
**To enable the mozilla.org test:** add to `matrix/cinny/config.json` homeserverList `"mozilla.org"`, and to the nginx CSP `connect-src`/`img-src`: `https://mozilla.org https://mozilla.modular.im https://chat.mozilla.org https://vector.im`.
---
## Priority 5 — Gamer / Aesthetic / Customization
### [MOVED] P5-9 · LFG (Looking for Group) Command → LotusBot
**Decision:** Implemented as `!lfg` in LotusBot rather than a client slash command. Bot-side rendering works consistently across all Matrix clients; client-side enhanced cards would only be visible to Lotus Chat users and require sanitizer auditing. The bot can also support richer flows (list active LFGs, DM interested players, auto-expire posts).
---
### [~] P5-15 · In-Call Soundboard — IMPLEMENTED (⚠️ awaiting live verification, D2-7)
**What:** Soundboard button in the call controls bar → popout grid of the user's clips; clicking one plays it **into the call** as a real published track (peers hear it) and locally (presser hears it). Clips are **user-uploadable, just like custom emojis/stickers**.
**🔱 [EC-FORK] Fork side + cinny side DONE.** The fork ships `io.lotus.inject_audio` (`LotusWidgetActions.InjectAudio`, allow-listed in `widget.ts`), armed via the `lotusAudioInject=1` flag; it publishes a clip as a separate LiveKit track — a **real** in-call soundboard mixed into the call, not local-only. cinny now drives it.
**Shipped (cinny):**
- Clips stored in `io.lotus.soundboard` account data → **synced across devices like emoji/sticker packs** (`useSoundboard` hook; `AccountDataEvent.LotusSoundboard`).
- Upload audio (≤1 MB, ≤40 clips) → `mx.uploadContent` → mxc; play resolves mxc → authed download → `blob:` object URL (the widget can't fetch authenticated media itself) → `control.injectAudio(url, volume)` + local playback.
- `CallSoundboard.tsx` popout in the call bar (upload / play / delete), gated on the `soundboardEnabled` setting (Settings → General → Calls, + volume slider).
**Remaining:** a dedicated Settings management page (optional — upload/delete already live in the popout); a small default clip set; live verification (D2-7). Files: `utils/soundboardClips.ts`, `hooks/useSoundboard.ts`, `features/call/CallSoundboard.tsx`, `plugins/call/CallControl.ts#injectAudio`.
**Complexity:** Medium — done.
---
### [~] P5-20 · Quick Reply from Browser Notification
**What:** Inline reply field in browser notification toasts via Notification Actions API. Reply sends as threaded reply to the triggering message.
**[AUDIT REQUIRED]** (1) Verify browser Notification Actions API support in target browsers. (2) Confirmed: service worker EXISTS at `src/sw.ts` — add `notificationclick` handler there.
**Complexity:** Medium-High.
**Partial Fix Applied ⚠️ UNTESTED:** Notifications now (a) show the real message body (`username: message` instead of "New inbox notification from..."), (b) click navigates directly to the room at the specific event (not the inbox), (c) `window.focus()` called on click so the tab comes to front, (d) reminder toasts also link to the specific event. Full inline-reply via Notification Actions API still needs the SW `push`+`notificationclick` pipeline (requires switching from `new Notification()` to `showNotification()` through the SW).
---
### [x] P5-30 · Advanced ML Noise Suppression (Krisp-style)
**What:** High-end background noise cancellation using a pre-trained ML model (RNNoise) running in the browser. Removes dogs, fans, and keyboard clicks from the mic stream.
**Shipped:** 3-tier setting (Off / Browser-native / ML) in Settings → General → Calls.
**🔱 [EC-FORK] DONE — moved in-source (2026-06).** ML denoise is now a first-class audio stage **inside** the forked Element Call: a LiveKit `TrackProcessor<Audio>` activated by `lotusDenoiseSource=1` (cinny sets it when ML is selected). The old build-time `getUserMedia`/`index.html` monkeypatch is **removed**. Because EC re-runs the processor on every (re)publish, denoise now **survives reconnects and mic-device switches** — this is the A7 fix (see `LOTUS_BUGS.md` A7, `LOTUS_TESTING.md` §D2-1). The processor degrades to the raw mic rather than going silent.
**Key decision:** LiveKit's Krisp filter is LiveKit-Cloud-only (we self-host the SFU); EC's own RNNoise PR #3892 is unmerged. Owning the fork let us implement the in-source stage directly.
**Models — all in-source in the fork:**
- [x] **DeepFilterNet 3** (48 kHz, **ML default**) · **DTLN** (16 kHz) · **RNNoise** (48 kHz) · **Speex** (48 kHz) — all four wired and selectable; dropdown ordered best-quality first. Tier default is **Browser-native**.
- [x] **Quality tuning (2026-07):** dry/wet **attenuation floor** (~-16 dB, RNNoise/Speex only — the "robotic" fix; DTLN/DFN would comb-filter), **gate-after-ML**, **DFN level 80→60**. Floor tunable via `lotusDenoiseFloor`.
- [x] **AEC/AGC (2026-07):** echo-cancellation ON; **AGC OFF for the ML tier** (`autoGainControl=false`, threaded through EC `UrlParams``ConnectionFactory`) so browser AGC doesn't fight the model; playback confirmed no AEC-defeat.
- [x] **Reliability (2026-07):** never-silent watchdog, resume-timeout, WASM-cache reject-eviction, activate-off-local-participant, init/build leak fixes.
- [ ] **Open verification:** real-call by-ear **A/B** — model choice, floor value, AGC on/off (RNNoise known-weak historically). `LOTUS_TESTING.md` §D2-1 / J2.
- [ ] **GTCRN (RESEARCHED — DEFERRED):** tiny MIT 16 kHz model that beats RNNoise, but **no drop-in browser package** — needs a ~1-week from-scratch build: `onnxruntime-web` (WASM, 1 thread) in a **Web Worker** (ORT can't run in an AudioWorklet — issue #13072) behind a custom AudioWorklet ring-buffer node presenting as an `AudioNode`; model `gtcrn_simple.onnx` (~300 KB, stateful — thread `conv/tra/inter` caches per frame); we write STFT/iSTFT (n_fft 512/hop 256). Assets ~34 MB via the `lotusDenoise()` vite plugin. Registration checklist known (both repos, incl. the 2nd `denoisePipeline.ts` used by the DenoiseTester). **Revisit only if low-power quality is insufficient after validating the current tuning.**
- [ ] **Desktop-only / HW-gated (future):** FRCRN or NVIDIA Maxine (RTX/Tensor only) — impossible in-browser; would run in the Tauri Rust backend + bridge a virtual mic into the webview. Detect capability; web falls back to RNNoise.
- **Excluded:** Krisp (LiveKit Cloud only); FRCRN/Maxine on web (GPU/server-bound).
---
### [~] P5-31 · Granular Voice & Screenshare Quality Controls — IMPLEMENTED (⚠️ awaiting live verification, D2-8)
**What:** Let users (and room admins) adjust audio bitrate and screenshare bitrate/framerate.
**🔱 [EC-FORK] Fork side + client side DONE.** The fork ships `io.lotus.set_quality` (`LotusWidgetActions.SetQuality`) that applies audio/screenshare encoding params (`RTCRtpSender.setParameters`, all simulcast encodings, re-applied on `TrackUnmuted`/republish) inside EC. cinny now drives it.
**Shipped (cinny):**
1. **User settings** (Settings → General → Calls): Microphone Bitrate, Screenshare Bitrate, Screenshare Framerate (`callAudioBitrate` / `screenshareBitrate` / `screenshareFramerate`).
2. **Room-admin caps**: `io.lotus.room_quality` state event (`StateEvent.LotusRoomQuality`) + `RoomQuality.tsx` in Room Settings → General → Voice (mirrors `RoomVoiceLimit`).
3. **Apply logic**: `useCallQuality` (wired in `CallEmbedProvider`'s `CallUtils`) builds `min(user setting, room cap)` and sends `io.lotus.set_quality` on join / when settings change (`utils/callQuality.ts`, unit-tested).
**Server-side enforcement (DONE — matrix repo):** extended `voice-limit-guard.py` (LXC 151) to also read `io.lotus.room_quality` and hard-enforce a **publish-source policy** for ALL clients.
- **Reality (researched, primary-source, LiveKit 1.9.11):** numeric bitrate/fps caps **cannot** be hard-enforced server-side — LiveKit is a pure SFU (forwards, never transcodes); there is NO bitrate/fps field in the JWT grant, `RoomConfiguration`, server `limit:` config, or any admin RPC, and stock Element Call ignores room metadata / custom claims for publish quality. So numeric caps stay **cooperative** (our fork honors them via `min()``set_quality`, already shipped).
- **What IS hard-enforced cross-client:** `VideoGrant.canPublishSources`. The guard holds the LiveKit secret, so when `io.lotus.room_quality` sets `allow_screenshare:false` / `allow_camera:false` it re-signs the issued JWT with a narrowed source list → the SFU refuses those tracks for **every** client (Element, FluffyChat, our fork). Mic always kept. Fail-open; unit-tested (`livekit/test_voice_limit_guard.py`). Admin UI: Room Settings → Voice → **Call Permissions** switches. cinny also hides the blocked buttons.
- **Live (mid-call) enforcement — DONE:** the JWT re-sign covers new joins; for participants **already in the call**, a background reconcile loop in the guard calls LiveKit `UpdateParticipant` every ~3 s to narrow `canPublishSources`, which unpublishes an in-progress screenshare/camera **server-side for all clients** and blocks re-publish (verified LiveKit 1.9.11 auto-unpublishes on permission narrowing). Only removes forbidden sources (never grants), preserves other permission flags, no-ops once compliant. So flipping a room audio-only kills live cameras/screenshares within ~one interval.
- **Not enforceable / deferred:** numeric server enforcement (impossible — see above); screenshare **resolution** control (`set_quality` covers bitrate + framerate; resolution needs a `getDisplayMedia` hook inside the fork).
**Complexity:** DONE — client (cooperative numeric caps) + server (hard publish-source policy). Only the physically-impossible numeric server enforcement is out of scope.
---
### [ ] P5-35 · Desktop — Notification Click Opens Room (DEFERRED)
**What:** Clicking a system tray notification navigates to the relevant room. Quick-reply from the notification toast would send the reply without opening the window.
**Status:** Deferred — `tauri-plugin-notification` has no Rust click/action callback API. Quick-reply would need a custom WinRT toast activator + COM registration, which can't be compile-tested without a Windows build environment.
**Note:** Tray icon and `matrix:` deep links already bring the window forward on most interactions. Revisit when tauri-plugin-notification gains click handler support upstream.
**Complexity:** High (platform-specific native code required).
---
### [ ] P5-36 · Desktop — Windows Jump List (DEFERRED)
**What:** Right-clicking the taskbar icon shows a jump list with recent/favorite rooms for quick navigation.
**Status:** Deferred — implementing the Windows COM jump list API in Tauri requires iterating on C++/COM code that can only be compile-checked on Windows, making blind CI iteration impractical.
**Action when unblocked:** Revisit when a Tauri plugin abstracts the Windows Shell `ICustomDestinationList` interface, or when a Windows build environment is available for local iteration.
**Complexity:** High (Windows-only native COM).
---
### [ ] P5-41 · Desktop — Native WinRT Toast Notifications
**What:** Replace emulated notifications with native WinRT Toast notifications.
**Approach:** Implement native WinRT Toast integration using `windows-rs` to enable full Action Center integration, including native Quick Reply functionality.
### [ ] P5-42 · Desktop — Persistent Background Sync
**What:** Maintain light connection to homeserver when WebView2 is suspended.
**Approach:** Implement a headless Rust sidecar to fetch unread counts/notifications while the webview is suspended to ensure instant notification delivery.
### [ ] P5-43 · Desktop — System Media Transport Controls (SMTC)
**What:** Integrate with Windows SMTC for volume flyout call/media control.
**Approach:** Use Windows SMTC API to expose call status, mic mute/unmute, and media controls to the Windows volume flyout/media overlay.
### [ ] P5-44 · Desktop — Taskbar Thumbnail Toolbar
**What:** Add persistent call controls to the taskbar preview.
**Approach:** Implement a COM thumbnail toolbar in the application preview window, featuring Mute/Deafen/End Call buttons.
### [ ] P5-46 · Desktop — System Power Management (Call Continuity)
**What:** Prevent system sleep/hibernate during active calls.
**Approach:** Use Tauri/Rust `power-manager` or platform-specific APIs to block system power saving states while a voice/video session is active.
### [ ] P5-47 · Desktop — TDS-Styled Native Window Chrome
**What:** Replace system titlebar with custom Lotus TDS chrome.
**Approach:** Configure Tauri window (`decorations: false`) and implement custom, TDS-token compliant titlebar controls (Close/Max/Min) for a cohesive UI.
### [ ] P5-48 · Desktop — Native File System Drag-and-Drop Improvements
**What:** Enhance drag-and-drop support for Windows.
**Approach:** Improve handling for Windows file shortcuts, recursive folder uploads, and shell-integrated "Send To" context menu actions.
### [ ] P5-49 · Desktop — Network Awareness (NCSI Integration)
**What:** Proactively detect Windows network connectivity changes.
**Approach:** Integrate with the Windows Network Connectivity Status Indicator (NCSI) API to improve offline mode transition latency and network recovery.
### [ ] P5-50 · Desktop — Windows Hardware-Accelerated Media Pipeline
**What:** Replace standard browser decoding with native Windows Media Foundation.
**Approach:** Leverage DirectShow/Media Foundation to offload video/audio decoding from the CPU to the GPU, significantly reducing power consumption and latency during calls.
### [ ] P5-51 · Desktop — Federated "Identity Contexts" (Isolation Manager)
**What:** Compartmentalize sessions, local databases, and caches into isolated "Contexts."
**Approach:** Implement a zero-leak boundary for personas (e.g., Work vs. Personal) by isolating `IndexedDB`, filesystem caches, and session persistence per context.
**Priority:** Extreme Low (Multi-sprint/Architectural).
### [~] P5-52 · Desktop — Room-Level Sync Governor (Performance Control) [STILL_CONSIDERING]
**What:** Granular sync tuning for individual rooms.
**Approach:** Allow per-room overrides for sync frequency and event type filtering (e.g., disable read receipts/typing in heavy rooms) to optimize performance. Implementation requires careful UX to prevent complexity fatigue.
### [ ] P5-53 · Desktop — Local-Only "Scripting" Plugin System (Tampermonkey-like)
**What:** A sandboxed environment for local execution of user scripts on Matrix events.
**Approach:** Implement a WASM-based execution engine that allows users to write local-only, client-side scripts to interact with incoming Matrix events, trigger sounds/notifications, or inject custom UI elements based on event payload rules. Designed for privacy — all logic runs exclusively on the local machine.
### [ ] P5-55 · Desktop — Composer Toolbar Drag-and-Drop Reordering
**What:** Allow users to reorder toolbar icons via drag-and-drop.
**Approach:** Extend the current settings-based toolbar toggle system to include a drag-and-drop UI mode in the composer settings, allowing users to personalize their icon order.
### [ ] P5-56 · Desktop — Windows "Focus Assist" (DND) Sync
**What:** Automatically toggle notification state based on Windows Focus Assist.
**Approach:** Integrate with the Windows `NotificationCenter` / `Focus` state via Tauri/Rust to automatically enable/disable Lotus Chat's internal notification suppression mode when Windows Focus Assist is toggled.
### [ ] P5-57 · Desktop — Visual Draft Persistence Indicator
---
## 🚀 Features to Add
- [ ] **Mobile Audit:** Comprehensive audit of all features in LOTUS_FEATURES.md for mobile PWA usability and layout responsiveness.
---
## Blocked Features
These features are confirmed desirable but cannot be built until the listed dependency is resolved.
Check back after each Synapse upgrade — re-run `/matrix/client/versions` and `unstable_features` to see if they've become available.
### [BLOCKED] · Live Location Sharing (MSC3489 + MSC3672)
**Blocked by:** `org.matrix.msc3489 = false` AND `org.matrix.msc3672 = false` on `matrix.lotusguild.org` (confirmed from unstable_features).
**What it would do:** Real-time GPS beacon streaming upgrading the existing static location share.
**Action when unblocked:** Both MSCs must be enabled on the homeserver before any client work.
### [BLOCKED] · Reaction / Relation Redaction (MSC3892)
**Blocked by:** `org.matrix.msc3892` = false on `matrix.lotusguild.org`
**What it would do:** Cleanly remove a reaction without redacting the parent message.
**Current behavior:** Full event redaction — acceptable fallback, no user-facing issue.
**Action when unblocked:** Find `onReactionToggle` redaction call site; swap in MSC3892 endpoint with fallback.
### [BLOCKED] · Room Preview Before Joining (MSC3266)
**Blocked by:** `GET /_matrix/client/v1/rooms/{roomId}/summary` returns `M_UNRECOGNIZED` 404 — endpoint not implemented in Synapse 1.155. Config flag `msc3266_enabled: true` is set but has no effect; Synapse appears not to have shipped a stable implementation at the v1 path. Verified 2026-06-18.
**What it would do:** Show room name, topic, avatar, member count before joining.
**Action when unblocked:** Re-test after each future Synapse upgrade.
### [BLOCKED] · Thread Subscriptions (MSC4306)
**Blocked by:** `org.matrix.msc4306` = false on `matrix.lotusguild.org`
**What it would do:** Follow a thread without posting; get notifications for replies.
**Action when unblocked:** Add "Follow thread" button in the thread panel header (depends on #P3-8 Thread Panel).
### [DONE] · Report User (MSC4260) ✅
**Previously blocked by:** Server spec v1.12, but `POST /_matrix/client/v3/users/{userId}/report` was confirmed **200** on 2026-06-18 (live since Synapse 1.133.0).
**What it does:** Reports a specific user to homeserver admins (separate from reporting a message).
**Note:** Report Message already exists in upstream Cinny. This adds Report User to the profile panel.
**Implemented 2026-06-18:** `ReportUserModal.tsx` added at `src/app/features/room/ReportUserModal.tsx`. Button wired into `UserRoomProfile.tsx` between UserModeration and UserDeviceSessions (hidden for own profile). Category dropdown + reason text, inline success/error feedback, auto-close 1500ms after success.
---
## Pending Audits
### [ ] Audit-3 · Profile banner image — Matrix protocol support
Research whether Matrix spec or MSC4133 (v1.16) defines a standard profile banner field. `uk.tcpip.msc4133.stable = true` on our server — check if a `banner_url` or similar field is defined. If no cross-client standard exists, do not implement.
---
## 📚 Implementation Reference
Exhaustive, low-level implementation details for backlog items. Follow these patterns to ensure code is "Lotus-perfect" (idiomatic, performant, and TDS-compliant).
### P3-8 · Thread Panel (Full Side Drawer)
**Architecture:** Mirror the `MembersDrawer` pattern but with a specialized timeline.
- **State (`src/app/state/room/thread.ts`):**
```typescript
export const activeThreadIdAtom = atom<string | null>(null);
```
- **Layout (`src/app/features/room/Room.tsx`):** Insert `ThreadPanel` conditionally alongside `RoomTimeline`:
```tsx
{
activeThreadId && (
<>
<Line variant="Background" direction="Vertical" size="300" />
<ThreadPanel roomId={roomId} threadId={activeThreadId} />
</>
);
}
```
- **Component (`src/app/features/room/thread/ThreadPanel.tsx`):** Use `room.getThread(threadId)` from the SDK. Render a `Header` with a "Close" button that sets `activeThreadIdAtom` to `null`. Reuse `RoomTimeline` but pass a filtered `EventTimelineSet`. Use `thread.timelineSet` directly for the most accurate thread view.
---
### P4-4 · Math / LaTeX Rendering
**Mechanism:** KaTeX injection into the HTML parser.
- **Sanitizer (`src/app/utils/sanitize.ts`):** Allow KaTeX-specific tags and classes (e.g., `span`, `annotation`, `math`). Use a specialized allowed list for math blocks.
> [Gemini_Found] `sanitize.ts` uses **`sanitize-html`** (not DOMPurify) with an explicit allowlist (`allowedTags`) and `disallowedTagsMode: 'discard'`. All MathML tags are currently absent from the allowlist and are silently stripped. Update `permittedHtmlTags` to include: `<math>`, `<mi>`, `<mo>`, `<mn>`, `<ms>`, `<mtext>`, `<mspace>`, `<mrow>`, `<mfrac>`, `<msqrt>`, `<mroot>`, `<mstyle>`, `<merror>`, `<mpadded>`, `<mphantom>`, `<mfenced>`, `<menclose>`, `<msub>`, `<msup>`, `<msubsup>`, `<munder>`, `<mover>`, `<munderover>`, `<mmultiscripts>`, `<mtable>`, `<mtr>`, `<mtd>`, `<maligngroup>`, `<malignmark>`, and `annotation`. Also add the required MathML attributes (e.g. `xmlns`, `display`, `mathvariant`) to `permittedTagToAttributes`.
- **Parser (`src/app/plugins/react-custom-html-parser.tsx`):** Detect `$ ... $` and `$$ ... $$` patterns in text nodes:
```tsx
if (node.type === 'text') {
const parts = node.data.split(/(\$\$.*?\$\$|\$.*?\$)/g);
return parts.map((p) => {
if (p.startsWith('$')) return <KaTeX math={p.replace(/\$/g, '')} />;
return p;
});
}
```
- **CSS (`src/app/styles/CustomHtml.css.ts`):** Import `katex/dist/katex.min.css` only when a math block is rendered to save initial bundle size.
---
### P4-6 · OIDC / SSO Next-Gen Auth (MSC3861)
**Mechanism:** Matrix Authentication Service (MAS) Integration.
- **Architecture:** Shift from password-based `/login` to OAuth2 `authorization_code` flow.
- **Key Files:** `src/app/pages/auth/Login.tsx` and `src/app/hooks/useAuth.ts`.
- **Implementation:** Use `oidc-client-ts` or a similar lightweight OIDC library. Check for `m.authentication` in `/.well-known/matrix/client`. Redirect to the MAS authorization endpoint. Handle the callback in a new `OidcCallback` route and store the OIDC `refresh_token`.
---
### P5-1 · Custom Accent Color Picker (Non-TDS only)
**Mechanism:** Dynamic CSS variable injection.
- **Setting (`src/app/state/settings.ts`):** Add `customAccentColor: string` (hex).
- **Manager (`src/app/pages/ThemeManager.tsx`):** Inside the `useEffect` that monitors theme changes:
```typescript
if (!lotusTerminal && customAccentColor) {
document.documentElement.style.setProperty('--lt-accent-orange', customAccentColor);
document.documentElement.style.setProperty('--lt-accent-orange-glow', `${customAccentColor}80`);
}
```
- **UI (`src/app/features/settings/general/General.tsx`):** Use `<Input type="color">`. Hide this section if `lotusTerminal` is `true`.
---
### P5-15 · In-Call Soundboard
**Mechanism:** Local-to-Global Audio Bridge via Web Audio API.
- Create an `AudioContext` and a `MediaStreamDestinationNode`.
- Create an `AudioBufferSourceNode` for each clip.
- Route the mic `MediaStream` and the clip source to the destination node.
- Pass the destination's `.stream` to the call bridge.
> ⚠️ **[Gemini_Found — CORRECTED]** Gemini originally suggested using LiveKit's `LocalAudioTrack.replaceTrack()` to mix audio into the call stream. This is **not possible** from Lotus Chat's realm: Element Call runs in a **cross-origin iframe** controlled via `matrix-widget-api` (postMessage). LiveKit's JS SDK and its `LocalAudioTrack` live inside EC's sandboxed context — inaccessible from our code. This directly contradicts the confirmed constraint already listed in the Server Capabilities table: _"Cindy CANNOT inject audio into EC call stream — In-call soundboard must be redesigned as local-only."_ The soundboard must be a local-playback-only feature (output through the user's speakers, not mixed into the call audio stream).
>
> 🔱 **[EC-FORK — RESOLVED]** Both the original claim and the earlier "practical blocker still holds" correction are now **outdated**. EC is same-origin **and** we own the source, so we no longer reach into EC's module scope from cinny — instead the fork **exposes the inject point itself**: the `io.lotus.inject_audio` widget action (`LotusWidgetActions.InjectAudio`) publishes a clip as a separate LiveKit track from inside EC. A **real** in-call soundboard (mixed into the call, not local-only) is therefore unblocked, and the cinny-side soundboard UI is now **built** (P5-15 above): uploadable clips played into the call via this action, stored in `io.lotus.soundboard` account data.
---
### P5-20 · Quick Reply from Browser Notification
**Mechanism:** Service Worker `notificationclick` Action.
> [Gemini_Found] Implementation detail: `serviceWorkerRegistration.showNotification()` should be used instead of `new Notification()` so that the service worker can listen to the `notificationclick` event. `new Notification()` creates notifications that are bound to the client page, not the SW.
```typescript
// src/sw.ts
self.addEventListener('notificationclick', (event) => {
if (event.action === 'reply' && event.reply) {
const { roomId, threadId } = event.notification.data;
const session = sessions.get(event.clientId);
fetch(`${session.baseUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message`, {
method: 'POST',
headers: { Authorization: `Bearer ${session.accessToken}` },
body: JSON.stringify({
msgtype: 'm.text',
body: event.reply,
'm.relates_to': threadId ? { rel_type: 'm.thread', event_id: threadId } : undefined,
}),
});
}
});
```
---
### P5-30 · Advanced ML Noise Suppression — Model Roadmap
See shipped implementation in LOTUS_FEATURES.md → "Noise Suppression (Advanced Multi-Tier)".
**Models status:**
- **RNNoise** (sapphi, 48 kHz) — ✅ working, default fallback. Keep — runs on any hardware.
- **Speex** (sapphi, 48 kHz) — ✅ working, low value; candidate to drop.
- **DTLN** (@workadventure, 16 kHz) — 🟡 wired; sample-rate fix applied (was robotic at 48 kHz). **TODO: verify in a real call.** Narrowband (16 kHz) = slightly telephone-y even when correct.
**Constraints:** client-side AudioWorklet, fully self-hosted, no GPU, self-hosted SFU (no LiveKit Cloud).
**Roadmap:**
- [ ] Verify DTLN 16 kHz fix in a real call.
- [ ] **DeepFilterNet 3** — best self-hostable upgrade: Rust→WASM, CPU real-time, 48 kHz fullband. Self-host `df_bg.wasm` + DFN3 ONNX model; wire a 48 kHz worklet. Audio quality unverifiable without a real-call test.
- [ ] **Desktop-only / HW-gated:** FRCRN (Alibaba) or NVIDIA Maxine (RTX/Tensor only). Runs in Tauri Rust backend + bridges a virtual mic into the webview. Must detect capability; web + weak HW falls back to RNNoise/DTLN.
---
### P5-31 · Granular Voice & Screenshare Quality Controls
**Mechanism:** WebRTC Encoding Parameters + Backend Quality Guard.
- **State Event:** `io.lotus.room_quality` (state key `""`) containing:
```json
{ "audio_bitrate": 128000, "screen_max_res": "1080p", "screen_max_fps": 60 }
```
- **Screenshare:** In `src/app/plugins/call/CallControl.ts`, map the "Quality" setting to `getDisplayMedia` constraints.
- **Audio Bitrate:** After the call joins, find the `RTCRtpSender` for the audio track:
```typescript
const sender = peerConnection.getSenders().find((s) => s.track?.kind === 'audio');
const params = sender.getParameters();
params.encodings[0].maxBitrate = roomBitrate || 128000;
await sender.setParameters(params);
```
- **Backend Sidecar:** Extend `voice-limit-guard.py` (LXC 151) to fetch `io.lotus.room_quality` and inject limits into the LiveKit JWT or return them as an authorized config packet.
---
### P5-40 · Desktop — Proactive Update Notifications (Tauri)
**Key Files:** `src/app/hooks/useTauriUpdater.ts`, `src/app/pages/client/ClientNonUIFeatures.tsx`, `src/app/features/toast/LotusToastContainer.tsx`.
1. Create a `TauriUpdateFeature` component. Use `useTauriUpdater()` to get the `check` function and `status`.
2. In a `useEffect`, call `check()` on mount and then on a `setInterval` (every 12 hours).
3. When status transitions to `{ state: 'available', version: '...' }`, fire a Lotus Toast: "Lotus Chat v[version] is available!" with an "Update" button that calls `install()`.
4. Store `lastCheck` timestamp in `localStorage` to prevent redundant checks on refresh.
---
### Mobile Bookmarks Visibility Fix
**Issue:** `ClientLayout.tsx` explicitly restricts `BookmarksPanel` to `ScreenSize.Desktop` (lines 51-56).
```tsx
// ClientLayout.tsx
{
bookmarksOpen && (
<BookmarksPanel
onClose={() => setBookmarksOpen(false)}
isMobile={screenSize !== ScreenSize.Desktop}
/>
);
}
```
`BookmarksPanel.tsx` already supports the `isMobile` prop (line 127) to enable full-screen absolute positioning. No other changes required.
---
### Remind Me Later (Slack-style)
**Mechanism:** Account Data + Timer/Service Worker.
- **Storage (`src/app/hooks/useReminders.ts`):** Store in account data `io.lotus.reminders` as `Array<{ id: string, roomId: string, eventId: string, timestamp: number }>`.
- **Context Menu (`src/app/features/room/message/MessageContextMenu.tsx`):** Add "Remind me" option → opens date/time picker modal (reuse `JumpToTime.tsx` logic).
- **Trigger (foreground):** `setTimeout` in a hook inside `ReminderMonitor` in `ClientNonUIFeatures.tsx` → pushes to `toastQueueAtom` in `state/toast.ts` when due.
- **Trigger (background):** Use Service Worker — `setTimeout` in the main thread will not fire when the PWA is suspended.
---
### Mobile Usability Audit — Methodology
1. **Viewport & Touch:** All interactive elements must have at least `44px × 44px` touch targets. Audit for horizontal overflow (horizontal scrolling must be disabled).
2. **Modal Responsiveness:** All modals (Settings, Profile, etc.) MUST cover the full screen on mobile, not float as overlays.
3. **Sidebar / Panels:** On mobile, sidebar panels (Members, Bookmarks, Media) must become full-screen overlays (using a `Drawer` or `Modal` pattern) rather than side-by-side flexbox panels.
4. **Input & Composer:** Ensure the composer doesn't get obscured by the mobile keyboard. Test focus trap and blur behaviors.
---
## Implementation Notes
### ⚠️ TDS DESIGN LAW (repeated here for emphasis)
> Every TDS color, animation, glow, border, shadow, and font value MUST come from `/root/code/web_template/base.css`.
> Never hardcode hex values. Never invent CSS variable names.
> Key variables: `--lt-accent-orange` · `--lt-accent-cyan` · `--lt-accent-green` · `--lt-glow-*` · `--lt-box-glow-*` · `--lt-border-color` · `--lt-font-mono`
> Reference implementation: `/root/code/tinker_tickets/` (markdown.js, base.js, ticket.css)
> This applies without exception to every task marked `[IMPROVE]`, `[Build]`, or any UI change.
### Design Rules
- All new components must respect both TDS dark (`LotusTerminalTheme`) and TDS light (`LotusTerminalLightTheme`) modes
- Non-TDS theme work (custom accent color, theme presets) uses vanilla-extract theme files — match the pattern in `src/lotus-terminal.css.ts`
- Code syntax highlighting token classes: `.tok-kw .tok-str .tok-num .tok-cmt .tok-fn` (defined in `web_template/base.css`)
- `folds AvatarImage` does NOT accept children — wrap Avatar components externally for overlays/frames/borders
### CI/CD Pipeline
```
edit → commit → git push origin lotus
→ Gitea Actions: tsc --noEmit, eslint, prettier (~3 min)
→ Webhook: lotus_deploy.sh on LXC 106 polls CI, then npm ci && npm run build → rsync
→ Live at chat.lotusguild.org (~11 min total)
```
### Per-Feature Checklist (before marking complete)
- [ ] `npx tsc --noEmit` — zero TypeScript errors
- [ ] `npx eslint src/` — zero new errors (warnings OK if pre-existing)
- [ ] `npx prettier --check src/` — formatting passes
- [ ] `README.md` updated (Lotus-custom features only — not upstream Cinny features)
- [ ] `landing/index.html` updated if the feature appears in the comparison table
- [ ] Visually tested at `chat.lotusguild.org` after CI deploys
### Homeserver Access (for server audits)
- **Synapse (Matrix):** LXC 151 on `compute-storage-01` — `pct exec 151 -- bash`
- **Config:** `/etc/matrix-synapse/homeserver.yaml`
- **Version check:** `curl -s https://matrix.lotusguild.org/_matrix/client/versions`
+99 -174
View File
@@ -1,186 +1,111 @@
# Lotus Chat
# Cinny
<p>
<a href="https://github.com/ajbura/cinny/releases">
<img alt="GitHub release downloads" src="https://img.shields.io/github/downloads/ajbura/cinny/total?logo=github&style=social"></a>
<a href="https://hub.docker.com/r/ajbura/cinny">
<img alt="DockerHub downloads" src="https://img.shields.io/docker/pulls/ajbura/cinny?logo=docker&style=social"></a>
<a href="https://fosstodon.org/@cinnyapp">
<img alt="Follow on Mastodon" src="https://img.shields.io/mastodon/follow/106845779685925461?domain=https%3A%2F%2Ffosstodon.org&logo=mastodon&style=social"></a>
<a href="https://twitter.com/intent/follow?screen_name=cinnyapp">
<img alt="Follow on Twitter" src="https://img.shields.io/twitter/follow/cinnyapp?logo=twitter&style=social"></a>
<a href="https://cinny.in/#sponsor">
<img alt="Sponsor Cinny" src="https://img.shields.io/opencollective/all/cinny?logo=opencollective&style=social"></a>
</p>
A Matrix chat client built for Lotus Guild — fast, private, and packed with the features you actually want.
A Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have an instant messaging application that is easy on people and has a modern touch.
- [Roadmap](https://github.com/orgs/cinnyapp/projects/1)
- [Contributing](./CONTRIBUTING.md)
**Deployed at [chat.lotusguild.org](https://chat.lotusguild.org)** &nbsp;|&nbsp; Forked from [Cinny](https://github.com/cinnyapp/cinny), synced through v4.12.3
<img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
---
## Getting started
The web app is available at [app.cinny.in](https://app.cinny.in/) and gets updated on each new release. The `dev` branch is continuously deployed at [dev.cinny.in](https://dev.cinny.in) but keep in mind that it could have things broken.
## Licensing & Attribution
You can also download our desktop app from the [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
The source code is licensed under [AGPLv3](LICENSE), the same license as the upstream Cinny project. The source for this fork is public at [code.lotusguild.org/LotusGuild/cinny](https://code.lotusguild.org/LotusGuild/cinny).
## Self-hosting
To host Cinny on your own, simply download the tarball from [GitHub releases](https://github.com/cinnyapp/cinny/releases/latest), and serve the files from `dist/` using your preferred webserver. Alternatively, you can just pull the docker image from [DockerHub](https://hub.docker.com/r/ajbura/cinny) or [GitHub Container Registry](https://github.com/cinnyapp/cinny/pkgs/container/cinny).
The Lotus Chat logo (`public/res/Lotus.png`) is a derivative work based on the original Cinny logo by Ajay Bura and contributors, used under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). The modified logo is © Lotus Guild and is also made available under CC BY 4.0.
* The default homeservers and explore pages are defined in [`config.json`](config.json).
---
* You need to set up redirects to serve the assests. Example configurations; [netlify](netlify.toml), [nginx](contrib/nginx/cinny.domain.tld.conf), [caddy](contrib/caddy/caddyfile).
* If you have trouble configuring redirects you can [enable hash routing](config.json#L35) — the url in the browser will have a `/#/` between the domain and open channel (ie. `app.cinny.in/#/home/` instead of `app.cinny.in/home/`) but you won't have to configure your webserver.
## Features
* To deploy on subdirectory, you need to rebuild the app youself after updating the `base` path in [`build.config.ts`](build.config.ts).
* For example, if you want to deploy on `https://cinny.in/app`, then set `base: '/app'`.
### Messaging
- See who has read each message, and track delivery status (sending / sent / failed)
- Bookmark any message and revisit saved messages from the sidebar
- Schedule messages to send at a specific time
- Click "edited" on any message to see the full edit history
- Drafts are saved automatically and survive page reloads
- Long messages collapse automatically — click "Read more" to expand
- Forward messages to other rooms
- Create and view polls directly in chat
- Share your location with an inline map embed
- Add captions to image and video uploads
- Optionally compress images before uploading — shows before/after file sizes
- GIF links from Giphy and Tenor auto-preview inline
- Search for and send GIFs from a built-in GIF picker
- Control voice message playback speed: 0.75× / 1× / 1.5× / 2×
- Search messages with a date range filter
- Room topics support rich formatting (bold, links, italics)
- Deleted messages show a placeholder instead of disappearing
- Code blocks highlight syntax for JS/TS, Python, and Rust
- Rich link preview cards for YouTube, GitHub, Twitter/X, Reddit, Spotify, Twitch, Steam, Wikipedia, Discord, npm, Stack Overflow, and IMDb
### Calls & Voice
- Push to Talk with a configurable keybind (default: Space)
- Push to Deafen with the M key
- Camera starts turned off by default when joining a call
- Screenshare requires confirmation before going live
- Toggle noise suppression on or off
- Calls float in a draggable picture-in-picture window when you navigate away
- Your chat background shows through the call view
- Dark/light mode inside calls matches your Lotus Chat theme
- Calls are available in DMs and private groups only — no accidental mass rings
- AFK auto-mute: mic is automatically silenced after a configurable idle timeout (130 min); a toast confirms the action
- Voice channel user limit: admins can cap how many people can be in a room's call — enforced server-side for every Matrix client (not just Lotus Chat); others see "Channel Full" until a spot opens
- Custom join/leave sound effects when someone enters or leaves your call — choose Chime, Soft, Retro, or off
- Soundboard: upload your own short audio clips (like custom emojis — they sync across your devices) and play them into a call so everyone hears them
- Call quality settings: cap your microphone bitrate, screenshare bitrate, and screenshare framerate — handy on a slow connection (Settings → Calls)
- Room call permissions: admins can turn off screen sharing or make a room audio-only (no cameras) — enforced server-side for every Matrix client, and it stops an in-progress share within seconds of being switched off
### Customization & Appearance
- LotusGuild Terminal Design System (TDS) — a CRT terminal-inspired dark theme
- TDS light mode variant for daytime use
- 20+ static chat background patterns
- 5 animated chat backgrounds: Digital Rain, Star Drift, Grid Pulse, Aurora Flow, Fireflies (with improved per-layer looping, phosphor-flicker rain, fluid aurora sweep, and organic firefly bioluminescence)
- 11 seasonal & holiday theme overlays — Halloween, Christmas, New Year, Autumn, Valentine's Day, St. Patrick's Day, Earth Day, Lunar New Year, April Fools', Deep Space, and Retro Arcade; auto-selected by date with a manual override in Settings → Appearance
- Avatar decorations — 99 animated APNG overlays (Gaming, Cyber, Space, Fantasy, Nature, Spooky, Cozy, and more) that frame your avatar across the timeline, members list, and @mention autocomplete; visible to all Lotus Chat users; select in Settings → Account → Avatar Decoration
- Toggle to pause background animations
- Glassmorphism sidebar — frosted glass effect that lets the background show through
- Night Light / blue light filter with an adjustable intensity slider
- Emoji prefixes on room names render larger in the sidebar (e.g. 🎮 general)
- Rename any room for yourself only — other members see the original name
- Emoji picker on all room name inputs
### Presence & Profile
- Discord-style presence selector: Online, Idle, Do Not Disturb, Invisible, or Auto
- Custom status message with emoji and an optional auto-clear timer (changing your status is never silently overwritten by activity events)
- Colored presence ring on member avatars (green / yellow / red)
- Profile fields for pronouns and timezone
- When a user's timezone is set, their current local time appears in their profile
- Private notes on any user's profile — freeform text visible only to you, auto-saves and syncs across devices
- Unread count shown in the browser tab title
### Moderation & Privacy
- Report any room to homeserver admins from the room menu
- View policy lists and ban lists (Draupnir-compatible, read-only)
- Toggle private read receipts so others can't see when you've read messages
- Optional warning when an encrypted room contains unverified devices
- Full push rule editor in notification settings
- View and edit Server ACL rules in room settings
- Filterable room activity / mod log (joins, kicks, bans, power level changes, etc.)
- Room stats and insights panel (active members, top reactions, media breakdown, activity heatmap)
- Export room history as plain text, JSON, or HTML with optional date range filter
### Notifications
- In-app toast notifications appear bottom-right when the window is focused
- Custom notification sounds per category (messages, invites)
- Quiet hours — suppress notifications during a configured time window
- Click a toast to jump directly to the room or DM
### UX
- Filter and search rooms in the sidebar
- Favorite rooms sync across devices and appear in a pinned section
- Sort rooms by recent activity, alphabetical, or unread first
- DM rows show a message preview and relative timestamp
- Right-click a room for a context menu: mute with duration, copy link, mark as read
- Quick emoji reactions appear on message hover — one click to react
- Knock-to-join: request access to a room; admins approve or deny from the members list
- Media gallery drawer: browse all images, videos, and files shared in a room
- Invite link and QR code in room settings
- Pending knock requests shown in the members list for room admins with a live badge count on the Members button
- Homeserver support contact displayed in Help & About (MSC1929)
- Server notice rooms are visually distinct from regular DMs
---
## Desktop App
Lotus Chat has a desktop app for Windows, macOS, and Linux. It wraps the same web client in a native window with automatic background updates — no need to reinstall for new versions.
### Download
Download the latest release from the [Releases page on code.lotusguild.org](https://code.lotusguild.org).
### SmartScreen Warning (Windows)
When you first run the installer on Windows, you may see a popup that says **"Windows protected your PC"** with the app listed as an unknown publisher. This is normal.
**Why it happens:** Windows SmartScreen flags any app that does not have an expensive commercial code-signing certificate from a major CA. Lotus Chat is signed with its own key for update verification, but that key is not in Microsoft's pre-approved list.
**How to install anyway:**
1. Click **"More info"** in the SmartScreen dialog.
2. A **"Run anyway"** button will appear.
3. Click it to proceed with installation.
After the first install, automatic in-app updates handle all future versions — you will not see this prompt again for updates.
---
## For Developers
The source code lives in `/root/code/cinny`. All changes should be made on the `lotus` branch. Push to `origin/lotus` and CI will automatically build and deploy to [chat.lotusguild.org](https://chat.lotusguild.org) in approximately 11 minutes — no manual build or deploy steps required.
See [LOTUS_FEATURES.md](LOTUS_FEATURES.md) for the full feature changelog and [LOTUS_TODO.md](LOTUS_TODO.md) for the work backlog.
### 🔱 Element Call fork ("Lotus Call") — LIVE
Voice/video channels embed **Element Call**, which is now our **self-built fork**
(`@lotusguild/element-call-embedded` `0.20.1-lotus.1`, source at
`LotusGuild/element-call`), published to our private Gitea npm registry and served
same-origin. We no longer depend on the upstream prebuilt bundle, so in-call
behavior is editable source instead of fragile DOM/widget hacks.
**Shipped via the fork:** denoise as an in-source LiveKit audio stage (survives
reconnects), in-call speaking/mute events, focus-a-participant during screenshare,
avatar decorations on EC video tiles, and a native transparent background.
**Built but dormant (need cinny UI):** real call-audio injection
(`io.lotus.inject_audio` → in-call soundboard) and quality controls
(`io.lotus.set_quality`).
The full plan and integration map is in
**[`HANDOFF_ELEMENT_CALL_FORK.md`](HANDOFF_ELEMENT_CALL_FORK.md)**; infra/hosting +
build-pipeline notes live in the `LotusGuild/matrix` repo README. Search the docs
for the **`[EC-FORK]`** tag to find every related note.
### Build
```bash
npm ci && npm run build # outputs to dist/
```
If the build is killed due to out-of-memory:
```bash
NODE_OPTIONS=--max_old_space_size=6144 npm run build
```
### CI/CD
<details><summary><b>PGP Public Key to verify tarball</b></summary>
```
edit → commit → git push → ~11 min → live at chat.lotusguild.org
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGJw/g0BDAC8qQeLqDMzYzfPyOmRlHVEoguVTo+eo1aVdQH2X7OELdjjBlyj
6d6c1adv/uF2g83NNMoQY7GEeHjRnXE4m8kYSaarb840pxrYUagDc0dAbJOGaCBY
FKTo7U1Kvg0vdiaRuus0pvc1NVdXSxRNQbFXBSwduD+zn66TI3HfcEHNN62FG1cE
K1jWDwLAU0P3kKmj8+CAc3h9ZklPu0k/+t5bf/LJkvdBJAUzGZpehbPL5f3u3BZ0
leZLIrR8uV7PiV5jKFahxlKR5KQHld8qQm+qVhYbUzpuMBGmh419I6UvTzxuRcvU
Frn9ttCEzV55Y+so4X2e4ZnB+5gOnNw+ecifGVdj/+UyWnqvqqDvLrEjjK890nLb
Pil4siecNMEpiwAN6WSmKpWaCwQAHEGDVeZCc/kT0iYfj5FBcsTVqWiO6eaxkUlm
jnulqWqRrlB8CJQQvih/g//uSEBdzIibo+ro+3Jpe120U/XVUH62i9HoRQEm6ADG
4zS5hIq4xyA8fL8AEQEAAbQdQ2lubnlBcHAgPGNpbm55YXBwQGdtYWlsLmNvbT6J
AdQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQSRri2MHidaaZv+
vvuUMwx6UK/M8wUCZqEDwAUJFvwIswAKCRCUMwx6UK/M877qC/4lxXOQIoWnLLkK
YiRCTkGsH6NdxgeYr6wpXT4xuQ45ZxCytwHpOGQmO/5up5961TxWW8D1frRIJHjj
AZGoRCL3EKEuY8nt3D99fpf3DvZrs1uoVAhiyn737hRlZAg+QsJheeGCmdSJ0hX5
Yud8SE+9zxLS1+CEjMrsUd/RGre/phme+wNXfaHfREAC9ewolgVChPIbMxG2f+vs
K8Xv52BFng7ta9fgsl1XuOjpuaSbQv6g+4ONk/lxKF0SmnhEGM3dmIYPONxW47Yf
atnIjRra/YhPTNwrNBGMmG4IFKaOsMbjW/eakjWTWOVKKJNBMoDdRcYYWIMCpLy8
AQUrMtQEsHSnqCwrw818S5A6rrhcfVGk36RGm0nOy6LS5g5jmqaYsvbCcBGY9B2c
SUAVNm17oo7TtEajk8hcSXoZod1t++pyjcVKEmSn3nFK7v5m3V+cPhNTxZMK459P
3x1Ucqj/kTqrxKw6s2Uknuk0ajmw0ljV+BQwgL6maguo9BKgCNW5AY0EYnD+DQEM
ANOu/d6ZMF8bW+Df9RDCUQKytbaZfa+ZbIHBus7whCD/SQMOhPKntv3HX7SmMCs+
5i27kJMu4YN623JCS7hdCoXVO1R5kXCEcneW/rPBMDutaM472YvIWMIqK9Wwl5+0
Piu2N+uTkKhe9uS2u7eN+Khef3d7xfjGRxoppM+xI9dZO+jhYiy8LuC0oBohTjJq
QPqfGDpowBwRkkOsGz/XVcesJ1Pzg4bKivTS9kZjZSyT9RRSY8As0sVUN57AwYul
s1+eh00n/tVpi2Jj9pCm7S0csSXvXj8v2OTdK1jt4YjpzR0/rwh4+/xlOjDjZEqH
vMPhpzpbgnwkxZ3X8BFne9dJ3maC5zQ3LAeCP5m1W0hXzagYhfyjo74slJgD1O8c
LDf2Oxc5MyM8Y/UK497zfqSPfgT3NhQmhHzk83DjXw3I6Z3A3U+Jp61w0eBRI1nx
H1UIG+gldcAKUTcfwL0lghoT3nmi9JAbvek0Smhz00Bbo8/dx8vwQRxDUxlt7Exx
NwARAQABiQG8BBgBCAAmAhsMFiEEka4tjB4nWmmb/r77lDMMelCvzPMFAmahA9IF
CRb8CMUACgkQlDMMelCvzPPQgQv/d5/z+fxgKqgfhQX+V49X4WgTVxZ/CzztDoJ1
XAq1dzTNEy8AFguXIo6eVXPSpMxec7ZreN3+UPQBnCf3eR5YxWNYOYKmk0G4E8D2
KGUJept7TSA42/8N2ov6tToXFg4CgzKZj0fYLwgutly7K8eiWmSU6ptaO8aEQBHB
gTGIOO3h6vJMGVycmoeRnHjv4wV84YWSVFSoJ7cY0he4Z9UznJBbE/KHZjrkXsPo
N+Gg5lDuOP5xjKzM5SogV9lhxBAhMWAg3URUF15yruZBiA8uV1FOK8sal/9C1G7V
M6ygA6uOZqXlZtcdA94RoSsW2pZ9eLVPsxz2B3Zko7tu11MpNP/wYmfGTI3KxZBj
n/eodvwjJSgHpGOFSmbNzvPJo3to5nNlp7wH1KxIMc6Uuu9hgfDfwkFZgV2bnFIa
Q6gyF548Ub48z7Dz83+WwLgbX19ve4oZx+dqSdczP6ILHRQomtrzrkkP2LU52oI5
mxFo+ioe/ABCufSmyqFye0psX3Sp
=WtqZ
-----END PGP PUBLIC KEY BLOCK-----
```
</details>
## Local development
> [!TIP]
> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Iron LTS (v20).
Execute the following commands to start a development server:
```sh
npm ci # Installs all dependencies
npm start # Serve a development version
```
To build the app:
```sh
npm run build # Compiles the app into the dist/ directory
```
### Running with Docker
This repository includes a Dockerfile, which builds the application from source and serves it with Nginx on port 80. To
use this locally, you can build the container like so:
```
docker build -t cinny:latest .
```
You can then run the container you've built with a command similar to this:
```
docker run -p 8080:80 cinny:latest
```
This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by navigating to `http://localhost:8080`.
Binary file not shown.
-357
View File
@@ -1,357 +0,0 @@
/*
* Lotus Chat — client-side ML noise suppression shim for Element Call.
*
* Element Call runs as a same-origin iframe widget that captures the mic
* internally (via livekit-client -> getUserMedia) and publishes it to LiveKit.
* We can't reach that track from the host. Instead this classic <script> is
* injected (by the vite `lotus-denoise` plugin) into EC's index.html BEFORE its
* deferred module entry, so it runs first and monkeypatches getUserMedia. When
* the "ml" tier is selected (lotusDenoise=ml in the widget URL) we route the
* captured mic through an RNNoise AudioWorklet (@sapphi-red/web-noise-suppressor)
* and hand the processed track back to EC/LiveKit.
*
* RNNoise REQUIRES mono, 48 kHz float audio. Feeding it anything else (stereo,
* or 44.1 kHz data the model treats as 48 kHz) produces loud static. So we:
* - run a 48 kHz AudioContext (which handles resampling from the hardware),
* - use the SIMD build if supported for better performance,
* - keep browser-native stationary suppression ON so the fans are removed
* before RNNoise focuses on transient noises (keyboard, dogs, etc.).
*
* Any failure falls back to the unprocessed mic so calls never break.
*/
(function () {
'use strict';
var params;
try {
params = new URLSearchParams(window.location.search);
if (params.get('lotusDenoise') !== 'ml') return;
} catch (e) {
return;
}
// Derive the parent origin for postMessage targetOrigin from the parentUrl
// widget param (a full URL) so denoise-status messages aren't broadcast with
// '*'. Fall back to this frame's own origin if parentUrl is missing/malformed.
var targetOrigin;
try {
var parentUrl = params.get('parentUrl');
targetOrigin = parentUrl ? new URL(parentUrl).origin : window.location.origin;
} catch (e) {
targetOrigin = window.location.origin;
}
var md = navigator.mediaDevices;
if (!md || typeof md.getUserMedia !== 'function') return;
if (typeof AudioWorkletNode === 'undefined' || typeof AudioContext === 'undefined') return;
var ASSET_BASE = './denoise/';
var MODEL = params.get('lotusModel') || 'rnnoise';
// DTLN (@workadventure) targets 16 kHz and does not resample internally, so
// its whole graph runs in a 16 kHz context; RNNoise/Speex (sapphi) and
// DeepFilterNet 3 are 48 kHz fullband. The processed MediaStreamTrack is
// published to LiveKit either way (WebRTC/Opus resamples as needed).
var SAMPLE_RATE = MODEL === 'dtln' ? 16000 : 48000;
var USE_NATIVE_NS = params.get('lotusNativeNS') === 'true';
var USE_GATE = params.get('lotusGate') === 'true';
var GATE_THRESHOLD = parseFloat(params.get('lotusGateThreshold') || '-45');
var PROCESSORS = {
rnnoise: {
name: '@sapphi-red/web-noise-suppressor/rnnoise',
script: 'rnnoiseWorklet.js',
wasm: 'rnnoise.wasm',
simdWasm: 'rnnoise_simd.wasm',
},
speex: {
name: '@sapphi-red/web-noise-suppressor/speex',
script: 'speexWorklet.js',
wasm: 'speex.wasm',
},
dtln: {
// @workadventure/noise-suppression is a self-contained ES module that
// resolves its own AudioWorklet processor + LiteRT WASM + TFLite models
// via import.meta.url. We dynamic-import this helper and let it build the
// node, rather than addModule-ing a flat worklet ourselves.
helper: 'workadventure/audio-worklet.js',
},
deepfilternet: {
// deepfilternet3-noise-filter ships an ESM whose AudioWorklet processor +
// wasm-bindgen glue are INLINED as a string (loaded via a Blob URL — no
// CDN for the worklet). The only assets it fetches are its single-threaded
// df_bg.wasm + ONNX model, which we vendor + self-host under
// deepfilternet/v2/... We dynamic-import the ESM, build a DeepFilterNet3Core
// pointed at the self-hosted base, and let it create the worklet node.
esm: 'deepfilternet/index.esm.js',
},
gate: {
name: '@sapphi-red/web-noise-suppressor/noise-gate',
script: 'noiseGateWorklet.js',
},
};
var origGetUserMedia = md.getUserMedia.bind(md);
var wasmPromises = {};
var ctxPromise = null;
function checkSimd() {
try {
return WebAssembly.validate(
new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0,
253, 15, 253, 98, 11,
]),
)
? Promise.resolve(true)
: Promise.resolve(false);
} catch (e) {
return Promise.resolve(false);
}
}
function loadWasm(modelId) {
if (wasmPromises[modelId]) return wasmPromises[modelId];
var p = PROCESSORS[modelId];
if (!p || !p.wasm) return Promise.resolve(null);
wasmPromises[modelId] = (modelId === 'rnnoise' ? checkSimd() : Promise.resolve(false)).then(
function (simd) {
var file = simd && p.simdWasm ? p.simdWasm : p.wasm;
return fetch(ASSET_BASE + file).then(function (r) {
if (!r.ok) {
if (simd && p.simdWasm)
return fetch(ASSET_BASE + p.wasm).then(function (r2) {
if (!r2.ok) throw new Error(modelId + ' wasm failed');
return r2.arrayBuffer();
});
throw new Error(modelId + ' wasm failed');
}
return r.arrayBuffer();
});
},
);
return wasmPromises[modelId];
}
function getContext() {
if (!ctxPromise) {
ctxPromise = (function () {
var ctx = new AudioContext({ sampleRate: SAMPLE_RATE });
if (ctx.sampleRate !== SAMPLE_RATE) {
try {
ctx.close();
} catch (e) {}
return Promise.reject(new Error('SampleRate mismatch: ' + ctx.sampleRate));
}
// Load worklet modules. DTLN registers its own processor via the
// dynamic-imported helper (see buildMlNode), so it needs nothing here.
var scripts = [];
if (MODEL === 'rnnoise' || MODEL === 'speex') scripts.push(PROCESSORS[MODEL].script);
if (USE_GATE) scripts.push(PROCESSORS.gate.script);
return Promise.all(
scripts.map(function (s) {
return ctx.audioWorklet.addModule(ASSET_BASE + s);
}),
).then(function () {
return ctx.state === 'suspended'
? ctx.resume().then(function () {
return ctx;
})
: ctx;
});
})();
ctxPromise.catch(function () {
ctxPromise = null;
});
}
return ctxPromise;
}
var hasNotifiedActive = false;
// Build the ML denoise AudioWorkletNode. RNNoise/Speex are flat sapphi
// worklets we instantiate directly with the fetched WASM binary. DTLN comes
// from @workadventure's self-contained helper, which we dynamic-import; it
// resolves its own processor + LiteRT WASM + TFLite models internally and
// returns the node. Resolves to { node, ready, dispose }.
function buildMlNode(ctx, wasmBinary) {
if (MODEL === 'dtln') {
return import(ASSET_BASE + PROCESSORS.dtln.helper).then(function (mod) {
// bypassUntilReady: pass raw audio through until the model is loaded so
// the call never has a silent/missing track during init.
return mod.createNoiseSuppressionAudioWorklet(ctx, { bypassUntilReady: true });
});
}
if (MODEL === 'deepfilternet') {
// Resolve an absolute self-hosted base so the package's cdnUrl override
// fetches our vendored df_bg.wasm + ONNX model (never the upstream CDN).
var dfnBase = new URL(ASSET_BASE + 'deepfilternet', window.location.href).href;
return import(ASSET_BASE + PROCESSORS.deepfilternet.esm).then(function (mod) {
var core = new mod.DeepFilterNet3Core({
sampleRate: SAMPLE_RATE,
noiseReductionLevel: 80,
assetConfig: { cdnUrl: dfnBase },
});
// initialize() fetches + compiles the wasm and loads the model on the
// main thread; the worklet node only exists once that resolves, so the
// graph is connected with a ready model (no half-initialised passthrough).
return core.initialize().then(function () {
return core.createAudioWorkletNode(ctx).then(function (node) {
return {
node: node,
ready: Promise.resolve(),
dispose: function () {
try {
core.destroy();
} catch (e) {}
},
};
});
});
});
}
var node = new AudioWorkletNode(ctx, PROCESSORS[MODEL].name, {
channelCount: 1,
numberOfInputs: 1,
numberOfOutputs: 1,
processorOptions: { maxChannels: 1, wasmBinary: wasmBinary },
});
return Promise.resolve({
node: node,
ready: Promise.resolve(),
dispose: function () {
try {
node.port.postMessage('destroy');
} catch (e) {}
},
});
}
function processStream(stream) {
var audioTracks = stream.getAudioTracks();
if (audioTracks.length === 0) return Promise.resolve(stream);
return Promise.all([loadWasm(MODEL), getContext()])
.then(function (res) {
var wasmBinary = res[0];
var ctx = res[1];
var source = ctx.createMediaStreamSource(stream);
var dest = ctx.createMediaStreamDestination();
var head = source;
// 1. Optional Noise Gate
if (USE_GATE) {
var gateNode = new AudioWorkletNode(ctx, PROCESSORS.gate.name, {
processorOptions: {
openThreshold: GATE_THRESHOLD,
closeThreshold: GATE_THRESHOLD - 5,
holdMs: 150,
maxChannels: 1,
},
});
head.connect(gateNode);
head = gateNode;
}
// 2. ML Processor
return buildMlNode(ctx, wasmBinary).then(function (ml) {
var mlNode = ml.node;
head.connect(mlNode);
mlNode.connect(dest);
// Surface async init failures (e.g. DTLN model load) without blocking
// the track handoff — audio flows via bypassUntilReady meanwhile.
if (ml.ready && typeof ml.ready.then === 'function') {
ml.ready.catch(function (err) {
var m = err instanceof Error ? err.message : String(err);
console.error('[lotus-denoise] ' + MODEL + ' init failed:', m);
});
}
var origTrack = audioTracks[0];
var processedTrack = dest.stream.getAudioTracks()[0];
var torndown = false;
function cleanup() {
if (torndown) return;
torndown = true;
try {
ml.dispose();
} catch (e) {}
try {
source.disconnect();
mlNode.disconnect();
} catch (e) {}
try {
if (gateNode) gateNode.disconnect();
} catch (e) {}
try {
origTrack.stop();
} catch (e) {}
}
var rawStop = processedTrack.stop.bind(processedTrack);
processedTrack.stop = function () {
cleanup();
rawStop();
};
origTrack.addEventListener('ended', function () {
try {
rawStop();
} catch (e) {}
cleanup();
});
if (!hasNotifiedActive) {
hasNotifiedActive = true;
window.parent.postMessage(
{
type: 'lotus-denoise-status',
active: true,
model: MODEL,
nativeNS: USE_NATIVE_NS,
gate: USE_GATE,
},
targetOrigin,
);
}
var out = new MediaStream();
out.addTrack(processedTrack);
stream.getVideoTracks().forEach(function (t) {
out.addTrack(t);
});
return out;
});
})
.catch(function (e) {
var msg = e instanceof Error ? e.message : String(e);
console.error('[lotus-denoise] Setup failed:', msg);
window.parent.postMessage(
{ type: 'lotus-denoise-status', active: false, error: msg },
targetOrigin,
);
return stream;
});
}
navigator.mediaDevices.getUserMedia = function (constraints) {
var wantsAudio = !!(constraints && constraints.audio);
var effective = constraints;
if (wantsAudio) {
var audioC =
typeof constraints.audio === 'object' ? Object.assign({}, constraints.audio) : {};
audioC.noiseSuppression = USE_NATIVE_NS;
audioC.channelCount = 1;
if (audioC.echoCancellation === undefined) audioC.echoCancellation = true;
if (audioC.autoGainControl === undefined) audioC.autoGainControl = true;
effective = Object.assign({}, constraints, { audio: audioC });
}
return origGetUserMedia(effective).then(function (stream) {
return wantsAudio ? processStream(stream) : stream;
});
};
})();
+29 -7
View File
@@ -1,16 +1,38 @@
{
"defaultHomeserver": 0,
"homeserverList": ["matrix.lotusguild.org", "matrix.org", "mozilla.org"],
"defaultHomeserver": 2,
"homeserverList": [
"converser.eu",
"envs.net",
"matrix.org",
"monero.social",
"mozilla.org",
"xmr.se"
],
"allowCustomHomeservers": true,
"featuredCommunities": {
"openAsDefault": false,
"spaces": [],
"rooms": [],
"servers": []
"spaces": [
"#cinny-space:matrix.org",
"#community:matrix.org",
"#space:envs.net",
"#science-space:matrix.org",
"#libregaming-games:tchncs.de",
"#mathematics-on:matrix.org"
],
"rooms": [
"#cinny:matrix.org",
"#freesoftware:matrix.org",
"#pcapdroid:matrix.org",
"#gentoo:matrix.org",
"#PrivSec.dev:arcticfoxes.net",
"#disroot:aria-net.org"
],
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
},
"hashRouter": {
"enabled": false,
"basename": "/"
},
"gifApiKey": ""
}
}
-112
View File
@@ -1,112 +0,0 @@
# Local OIDC / next-gen-auth (MSC3861) test loop
The Lotus client gained MSC3861/MSC2965 OIDC login (P4-6). lotusguild's own
homeserver is **not** MSC3861, so to exercise the flow without a mozilla.org
tester you need a local homeserver that delegates auth to a **Matrix
Authentication Service (MAS)**. This is the dev loop.
> Status: the Lotus-client side is unit-tested + gate-green; this server loop is
> the manual end-to-end check. It hasn't been run in CI (no container runtime
> there), so treat version pins as a starting point and bump as needed.
## 1. Stand up MAS + Synapse
The simplest path is the **upstream MAS docker-compose quickstart** — it's
maintained and handles key generation + the database:
<https://element-hq.github.io/matrix-authentication-service/setup/installation.html>
(`docker compose` section). Use it to get MAS + Synapse + Postgres running, then
apply the two Lotus-specific deltas below.
A minimal `compose.yaml` skeleton (generate MAS keys first — do **not** hand-write them):
```yaml
services:
postgres:
image: postgres:16
environment: { POSTGRES_USER: synapse, POSTGRES_PASSWORD: pw, POSTGRES_DB: synapse }
mas:
image: ghcr.io/element-hq/matrix-authentication-service:latest
command: server
ports: ['8090:8080'] # MAS issuer on http://localhost:8090
volumes: ['./mas:/data']
# First run once: `docker compose run --rm mas config generate -o /data/config.yaml`
# then edit /data/mas/config.yaml (see §1a) before `up`.
synapse:
image: ghcr.io/element-hq/synapse:latest
ports: ['8008:8008'] # client/federation API
volumes: ['./synapse:/data']
depends_on: [postgres, mas]
```
### 1a. MAS `config.yaml` — the parts that matter
After `config generate` (which fills in `secrets.keys` + `encryption`), set:
```yaml
http:
public_base: http://localhost:8090/
issuer: http://localhost:8090/
database:
uri: postgresql://synapse:pw@postgres/synapse
matrix:
homeserver: localhost # the server_name
endpoint: http://synapse:8008/
secret: "REPLACE_WITH_A_LONG_SHARED_ADMIN_TOKEN"
clients:
- client_id: "0000000000000000000SYNAPSE"
client_auth_method: client_secret_basic
client_secret: "REPLACE_WITH_A_SHARED_CLIENT_SECRET"
passwords: # so you can create a local test account in the MAS UI
enabled: true
```
### 1b. Synapse `homeserver.yaml` — delegate auth to MAS
See `synapse-msc3861.yaml` in this folder; the key block is:
```yaml
experimental_features:
msc3861:
enabled: true
issuer: http://localhost:8090/
client_id: "0000000000000000000SYNAPSE"
client_auth_method: client_secret_basic
client_secret: "REPLACE_WITH_A_SHARED_CLIENT_SECRET" # == MAS clients[].client_secret
admin_token: "REPLACE_WITH_A_LONG_SHARED_ADMIN_TOKEN" # == MAS matrix.secret
account_management_url: "http://localhost:8090/account"
```
Create a test user via the MAS UI (`http://localhost:8090/`) or
`docker compose exec mas mas-cli manage register-user`.
Sanity check discovery (the client relies on this):
```bash
curl -s http://localhost:8008/.well-known/matrix/client | jq '."m.authentication"'
# -> { "issuer": "http://localhost:8090/", "account": "http://localhost:8090/account" }
```
## 2. Point the Lotus dev client at it
Run the client: `npm start` (vite dev). Override `public/config.json` so the
local server is selectable and custom servers are allowed:
```json
{
"defaultHomeserver": 0,
"homeserverList": ["localhost:8008"],
"allowCustomHomeservers": true,
"hashRouter": { "enabled": false, "basename": "/" }
}
```
Dynamic client registration handles the redirect URI automatically — it's
`<vite-origin>/auth/oidc/callback` (e.g. `http://localhost:5173/auth/oidc/callback`),
and MAS allows `http://localhost` redirects in dev.
## 3. Run the checklist
See **section N** of `../../LOTUS_TESTING.md` for the actual pass/fail steps
(login redirect, callback, session-persist-on-reload, token refresh, logout
revocation, account-management link, and the non-OIDC-regression check).
## Files here
- `synapse-msc3861.yaml` — the Synapse experimental-features delta.
- `config.local.json` — the Lotus `public/config.json` override.
-7
View File
@@ -1,7 +0,0 @@
{
"defaultHomeserver": 0,
"homeserverList": ["localhost:8008"],
"allowCustomHomeservers": true,
"featuredCommunities": { "openAsDefault": false, "spaces": [], "rooms": [], "servers": [] },
"hashRouter": { "enabled": false, "basename": "/" }
}
-16
View File
@@ -1,16 +0,0 @@
# Synapse experimental-features delta to delegate auth to a local MAS (MSC3861).
# Merge this into your test homeserver.yaml. The client_secret + admin_token MUST
# match the MAS config (clients[].client_secret and matrix.secret respectively).
experimental_features:
msc3861:
enabled: true
issuer: http://localhost:8090/
client_id: "0000000000000000000SYNAPSE"
client_auth_method: client_secret_basic
client_secret: "REPLACE_WITH_A_SHARED_CLIENT_SECRET"
admin_token: "REPLACE_WITH_A_LONG_SHARED_ADMIN_TOKEN"
account_management_url: "http://localhost:8090/account"
# With msc3861 enabled, Synapse disables its own password/SSO login and advertises
# `m.authentication` in /.well-known/matrix/client — which is exactly what the
# Lotus client's getOidcIssuer() reads to switch into the OIDC flow.
-126
View File
@@ -1,126 +0,0 @@
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
import eslintConfigPrettier from 'eslint-config-prettier';
import globals from 'globals';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
{ ignores: ['node_modules/**', 'dist/**', 'experiment/**'] },
js.configs.recommended,
tsPlugin.configs['flat/eslint-recommended'],
...tsPlugin.configs['flat/recommended'],
reactPlugin.configs.flat.recommended,
reactHooksPlugin.configs.flat['recommended'],
// Register jsx-a11y plugin (rules selectively enabled below)
{ plugins: { 'jsx-a11y': jsxA11yPlugin } },
// airbnb-base via FlatCompat (JS/import rules; no React plugin, no getFilename issue)
...compat.extends('airbnb-base'),
eslintConfigPrettier,
{
languageOptions: {
parser: tsParser,
globals: {
...globals.browser,
...globals.es2021,
JSX: 'readonly',
},
parserOptions: {
ecmaFeatures: { jsx: true },
ecmaVersion: 'latest',
sourceType: 'module',
},
},
settings: {
react: {
version: '18.2.0',
},
},
rules: {
'linebreak-style': 0,
'no-unused-vars': 'off', // handled by @typescript-eslint/no-unused-vars
'no-underscore-dangle': 0,
'no-shadow': 'off',
// Stylistic rules — off for this codebase
'no-console': 'off',
'no-continue': 'off',
'no-nested-ternary': 'off',
'no-plusplus': 'off',
'no-param-reassign': 'off',
'no-restricted-syntax': 'off',
'no-restricted-globals': 'off',
'no-constant-condition': 'off',
'prefer-destructuring': 'off',
'no-useless-assignment': 'off',
'preserve-caught-error': 'off',
'consistent-return': 'off',
'no-use-before-define': 'off',
'import/prefer-default-export': 'off',
'import/extensions': 'off',
'import/no-unresolved': 'off',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
},
],
'react/no-unstable-nested-components': ['error', { allowAsProps: true }],
'react/jsx-filename-extension': [
'error',
{
extensions: ['.tsx', '.jsx'],
},
],
'react/display-name': 'off',
'react/require-default-props': 'off',
'react/jsx-props-no-spreading': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
// React Compiler rules added in react-hooks v7 — disabled until React Compiler is adopted
'react-hooks/react-compiler': 'off',
'react-hooks/incompatible-library': 'off',
'react-hooks/refs': 'off',
'react-hooks/set-state-in-effect': 'off',
'react-hooks/set-state-in-render': 'off',
'react-hooks/immutability': 'off',
'react-hooks/purity': 'off',
'react-hooks/use-memo': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
],
'@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
// jsx-a11y — media captions not required for this app
'jsx-a11y/media-has-caption': 'off',
'jsx-a11y/no-noninteractive-element-interactions': 'off',
'jsx-a11y/alt-text': 'off',
},
},
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'no-undef': 'off',
},
},
];
+15 -22
View File
@@ -1,42 +1,36 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lotus Chat</title>
<meta name="name" content="Lotus Chat" />
<meta name="author" content="Lotus Guild" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>Cinny</title>
<meta name="name" content="Cinny" />
<meta name="author" content="Ajay Bura" />
<meta
name="description"
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
/>
<meta name="keywords" content="lotus chat, lotus guild, matrix, matrix client" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Lotus Chat" />
<meta property="og:url" content="https://chat.lotusguild.org" />
<meta
property="og:image"
content="https://chat.lotusguild.org/public/res/android/android-chrome-192x192.png"
name="keywords"
content="cinny, cinnyapp, cinnychat, matrix, matrix client, matrix.org, element"
/>
<meta property="og:title" content="Cinny" />
<meta property="og:url" content="https://cinny.in" />
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png" />
<meta
property="og:description"
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
/>
<meta name="theme-color" content="#000000" />
<meta name="color-scheme" content="dark light" />
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/fonts/custom-fonts.css" />
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
<link rel="manifest" href="/manifest.json" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="application-name" content="Lotus Chat" />
<meta name="apple-mobile-web-app-title" content="Lotus Chat" />
<meta name="application-name" content="Cinny" />
<meta name="apple-mobile-web-app-title" content="Cinny" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
@@ -96,7 +90,6 @@
window.global ||= window;
</script>
<div id="root"></div>
<div id="portalContainer"></div>
<script type="module" src="./src/index.tsx"></script>
</body>
</html>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 851 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 KiB

+5055 -6104
View File
File diff suppressed because it is too large Load Diff
+86 -116
View File
@@ -1,7 +1,7 @@
{
"name": "lotus-chat",
"version": "4.12.3-lotus",
"description": "Lotus Chat — Matrix client for Lotus Guild",
"name": "cinny",
"version": "4.8.1",
"description": "Yet another matrix client",
"main": "index.js",
"type": "module",
"engines": {
@@ -10,139 +10,109 @@
"scripts": {
"start": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "npm run check:eslint && npm run check:prettier",
"lint": "yarn check:eslint && yarn check:prettier",
"check:eslint": "eslint src/*",
"check:prettier": "prettier --check .",
"fix:prettier": "prettier --write .",
"typecheck": "tsc --noEmit",
"test": "node --import tsx --test $(find src -name '*.test.ts')",
"prepare": "husky",
"commit": "git-cz",
"postinstall": "node scripts/patch-folds.mjs",
"sync:decorations": "node scripts/syncDecorations.mjs"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": "eslint",
"*": "prettier --ignore-unknown --write"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
"typecheck": "tsc --noEmit"
},
"keywords": [],
"author": "Ajay Bura",
"license": "AGPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.8.1",
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "2.1.5",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
"@eslint/eslintrc": "3.3.5",
"@eslint/js": "10.0.1",
"@fontsource-variable/inter": "5.2.8",
"@giphy/js-fetch-api": "5.8.0",
"@giphy/js-types": "5.1.0",
"@giphy/js-util": "5.2.0",
"@giphy/react-components": "10.1.2",
"@sapphi-red/web-noise-suppressor": "0.3.5",
"@tanstack/react-query": "5.100.13",
"@tanstack/react-query-devtools": "5.100.13",
"@tanstack/react-virtual": "3.13.25",
"@types/dompurify": "3.2.0",
"@workadventure/noise-suppression": "0.0.4",
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
"@fontsource/inter": "4.5.14",
"@tanstack/react-query": "5.24.1",
"@tanstack/react-query-devtools": "5.24.1",
"@tanstack/react-virtual": "3.2.0",
"@tippyjs/react": "4.2.6",
"@vanilla-extract/css": "1.9.3",
"@vanilla-extract/recipes": "0.3.0",
"@vanilla-extract/vite-plugin": "3.7.1",
"await-to-js": "3.0.0",
"badwords-list": "2.0.1-4",
"blurhash": "2.0.5",
"blurhash": "2.0.4",
"browser-encrypt-attachment": "0.3.0",
"chroma-js": "3.2.0",
"classnames": "2.5.1",
"chroma-js": "3.1.2",
"classnames": "2.3.2",
"dateformat": "5.0.3",
"dayjs": "1.11.20",
"deepfilternet3-noise-filter": "1.2.1",
"domhandler": "6.0.1",
"dompurify": "3.4.5",
"emojibase": "17.0.0",
"emojibase-data": "17.0.0",
"dayjs": "1.11.10",
"domhandler": "5.0.3",
"emojibase": "15.3.1",
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "12.0.2",
"folds": "2.6.2",
"globals": "17.6.0",
"html-dom-parser": "7.1.0",
"html-react-parser": "6.1.2",
"i18next": "26.2.0",
"i18next-browser-languagedetector": "8.2.1",
"i18next-http-backend": "4.0.0",
"immer": "11.1.8",
"flux": "4.0.3",
"focus-trap-react": "10.0.2",
"folds": "2.2.0",
"formik": "2.4.6",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
"i18next-browser-languagedetector": "8.0.0",
"i18next-http-backend": "2.5.2",
"immer": "9.0.16",
"is-hotkey": "0.2.0",
"jotai": "2.20.0",
"linkify-react": "4.3.3",
"linkifyjs": "4.3.3",
"matrix-js-sdk": "41.6.0-rc.0",
"matrix-widget-api": "1.17.0",
"jotai": "2.6.0",
"linkify-react": "4.1.3",
"linkifyjs": "4.1.3",
"matrix-js-sdk": "37.5.0",
"millify": "6.1.0",
"pdfjs-dist": "5.7.284",
"pdfjs-dist": "4.2.67",
"prismjs": "1.30.0",
"react": "19.2.6",
"react-aria": "3.48.0",
"react-blurhash": "0.3.0",
"react-colorful": "5.7.0",
"react-dom": "19.2.6",
"react-error-boundary": "6.1.1",
"react-google-recaptcha": "3.1.0",
"react-i18next": "17.0.8",
"react-range": "1.10.0",
"react-router-dom": "7.15.1",
"sanitize-html": "2.17.4",
"slate": "0.124.1",
"slate-dom": "0.124.1",
"slate-history": "0.113.1",
"slate-react": "0.124.2",
"styled-components": "6.4.2",
"ua-parser-js": "2.0.10"
"prop-types": "15.8.1",
"react": "18.2.0",
"react-aria": "3.29.1",
"react-autosize-textarea": "7.1.0",
"react-blurhash": "0.2.0",
"react-colorful": "5.6.1",
"react-dom": "18.2.0",
"react-error-boundary": "4.0.13",
"react-google-recaptcha": "2.1.0",
"react-i18next": "15.0.0",
"react-modal": "3.16.1",
"react-range": "1.8.14",
"react-router-dom": "6.20.0",
"sanitize-html": "2.12.1",
"slate": "0.112.0",
"slate-dom": "0.112.2",
"slate-history": "0.110.3",
"slate-react": "0.112.1",
"tippy.js": "6.3.7",
"ua-parser-js": "1.0.35"
},
"devDependencies": {
"@lotusguild/element-call-embedded": "0.20.1-lotus.1",
"@rollup/plugin-inject": "5.0.5",
"@rollup/plugin-wasm": "6.2.2",
"@types/chroma-js": "3.1.2",
"@types/file-saver": "2.0.7",
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@rollup/plugin-inject": "5.0.3",
"@rollup/plugin-wasm": "6.1.1",
"@types/chroma-js": "3.1.1",
"@types/file-saver": "2.0.5",
"@types/is-hotkey": "0.1.10",
"@types/node": "25.9.1",
"@types/prismjs": "1.26.6",
"@types/react": "19.2.15",
"@types/react-dom": "19.2.3",
"@types/react-google-recaptcha": "2.1.9",
"@types/sanitize-html": "2.16.1",
"@types/ua-parser-js": "0.7.39",
"@typescript-eslint/eslint-plugin": "8.59.4",
"@typescript-eslint/parser": "8.59.4",
"@vanilla-extract/css": "1.20.1",
"@vanilla-extract/recipes": "0.5.7",
"@vanilla-extract/vite-plugin": "5.2.2",
"@vitejs/plugin-react": "6.0.2",
"@types/node": "18.11.18",
"@types/prismjs": "1.26.0",
"@types/react": "18.2.39",
"@types/react-dom": "18.2.17",
"@types/react-google-recaptcha": "2.1.8",
"@types/sanitize-html": "2.9.0",
"@types/ua-parser-js": "0.7.36",
"@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1",
"@vitejs/plugin-react": "4.2.0",
"buffer": "6.0.3",
"cz-conventional-changelog": "3.3.0",
"eslint": "9.39.4",
"eslint": "8.29.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.1.1",
"husky": "9.1.7",
"lint-staged": "17.0.5",
"prettier": "3.8.3",
"tsx": "4.22.4",
"typescript": "6.0.3",
"vite": "8.0.14",
"vite-plugin-pwa": "1.3.0",
"vite-plugin-static-copy": "4.1.0"
},
"overrides": {
"@giphy/js-util": {
"dompurify": ">=3.3.4"
},
"js-cookie": ">=3.0.6"
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0",
"prettier": "2.8.1",
"sass": "1.56.2",
"typescript": "4.9.4",
"vite": "5.4.19",
"vite-plugin-pwa": "0.20.5",
"vite-plugin-static-copy": "1.0.4",
"vite-plugin-top-level-await": "1.4.4"
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"defaultHomeserver": 0,
"homeserverList": ["matrix.lotusguild.org", "matrix.org", "mozilla.org"],
"allowCustomHomeservers": true,
"featuredCommunities": {
"openAsDefault": false,
"spaces": [],
"rooms": [],
"servers": []
},
"hashRouter": {
"enabled": false,
"basename": "/"
},
"gifApiKey": ""
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.
Binary file not shown.
-11
View File
@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>Thats an error.</ins>
<p>The requested URL <code>/s/jetbrainsmono/v18/tDbY2o-flEEny0FZhsfKu5WU4xD-IQ.woff2</code> was not found on this server. <ins>Thats all we know.</ins>
-11
View File
@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>Thats an error.</ins>
<p>The requested URL <code>/s/jetbrainsmono/v18/tDbY2o-flEEny0FZhsfKu5WU4xD-IQ.woff2</code> was not found on this server. <ins>Thats all we know.</ins>
-11
View File
@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>Thats an error.</ins>
<p>The requested URL <code>/s/jetbrainsmono/v18/tDbY2o-flEEny0FZhsfKu5WU4xD-IQ.woff2</code> was not found on this server. <ins>Thats all we know.</ins>
Binary file not shown.
Binary file not shown.
Binary file not shown.
-51
View File
@@ -1,51 +0,0 @@
/* Self-hosted fonts — avoids tracking prevention in desktop WebView2 */
@font-face {
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/fonts/JetBrainsMono-italic-400.woff2') format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/JetBrainsMono-normal-400.woff2') format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/JetBrainsMono-normal-700.woff2') format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Fira Code';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/FiraCode-400.woff2') format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Fira Code';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/FiraCode-600.woff2') format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
-51
View File
@@ -2,57 +2,6 @@
"Organisms": {
"RoomCommon": {
"changed_room_name": " changed room name"
},
"CreateRoom": {
"chat_room": "Chat Room",
"chat_room_desc": "Messages, photos, and videos.",
"voice_room": "Voice Room",
"voice_room_desc": "Live audio and video conversations."
},
"ImageViewer": {
"download": "Download"
},
"Message": {
"open_location": "Open Location",
"thread": "Thread"
},
"ImageContent": {
"view": "View",
"spoiler": "Spoiler",
"retry": "Retry"
},
"DeviceVerification": {
"close": "Close",
"accept": "Accept",
"they_match": "They Match",
"okay": "Okay",
"do_not_match": "Do not Match",
"please_accept": "Please accept the request from other device.",
"waiting_accept": "Waiting for request to be accepted...",
"click_accept": "Click accept to start the verification process.",
"request_accepted": "Verification request has been accepted.",
"waiting_response": "Waiting for the response from other device...",
"starting_emoji": "Starting verification using emoji comparison...",
"confirm_emoji": "Confirm the emoji below are displayed on both devices, in the same order:",
"device_verified": "Your device is verified.",
"verification_canceled": "Verification has been canceled."
},
"UrlPreview": {
"join_server": "Join Server"
},
"InviteUser": {
"invite": "Invite"
},
"UploadBoard": {
"files": "Files",
"send": "Send",
"upload_failed": "Upload Failed"
},
"PasswordStage": {
"account_password": "Account Password",
"password": "Password",
"invalid_password": "Invalid Password!",
"authenticate_prompt": "To perform this action you need to authenticate yourself by entering you account password."
}
}
}
+14 -41
View File
@@ -1,86 +1,59 @@
{
"name": "Lotus Chat",
"short_name": "Lotus Chat",
"description": "Lotus Chat \u2014 the Lotus Guild Matrix client",
"name": "Cinny",
"short_name": "Cinny",
"description": "Yet another matrix client",
"dir": "auto",
"lang": "en-US",
"display": "standalone",
"orientation": "portrait",
"start_url": "./",
"background_color": "#0a0a0a",
"theme_color": "#980000",
"background_color": "#fff",
"theme_color": "#fff",
"icons": [
{
"src": "./res/android/android-chrome-36x36.png",
"src": "./public/android/android-chrome-36x36.png",
"sizes": "36x36",
"type": "image/png"
},
{
"src": "./res/android/android-chrome-48x48.png",
"src": "./public/android/android-chrome-48x48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "./res/android/android-chrome-72x72.png",
"src": "./public/android/android-chrome-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "./res/android/android-chrome-96x96.png",
"src": "./public/android/android-chrome-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "./res/android/android-chrome-144x144.png",
"src": "./public/android/android-chrome-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "./res/android/android-chrome-192x192.png",
"src": "./public/android/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./res/android/android-chrome-256x256.png",
"src": "./public/android/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "./res/android/android-chrome-384x384.png",
"src": "./public/android/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "./res/android/android-chrome-512x512.png",
"src": "./public/android/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "./res/android/maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./res/android/maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"categories": ["social", "communication", "productivity"],
"shortcuts": [
{
"name": "New Message",
"short_name": "DM",
"description": "Open a new direct message",
"url": "/",
"icons": [
{
"src": "res/android/android-chrome-96x96.png",
"sizes": "96x96"
}
]
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<path fill="#231F20" d="M9,11H5c-1.1,0-2-0.9-2-2V5c0-1.1,0.9-2,2-2h4c1.1,0,2,0.9,2,2v4C11,10.1,10.1,11,9,11z"/>
</g>
<g>
<path fill="#231F20" d="M19,11h-4c-1.1,0-2-0.9-2-2V5c0-1.1,0.9-2,2-2h4c1.1,0,2,0.9,2,2v4C21,10.1,20.1,11,19,11z"/>
</g>
<g>
<path fill="#231F20" d="M9,21H5c-1.1,0-2-0.9-2-2v-4c0-1.1,0.9-2,2-2h4c1.1,0,2,0.9,2,2v4C11,20.1,10.1,21,9,21z"/>
</g>
<g>
<path fill="#231F20" d="M19,21h-4c-1.1,0-2-0.9-2-2v-4c0-1.1,0.9-2,2-2h4c1.1,0,2,0.9,2,2v4C21,20.1,20.1,21,19,21z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 940 B

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path d="M13.8,4.5l0.7,0.7l-3.4,3.4L7.7,9.7l-1-1l-1.4,1.4l3.5,3.5l-5.7,5.7l1.4,1.4l5.7-5.7l3.5,3.5l1.4-1.4l-1-1l1.1-3.4l3.4-3.4
l0.7,0.7l1.4-1.4l-5.7-5.7L13.8,4.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 612 B

+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<polygon points="12,2 15.1,8.6 22,9.6 17,14.8 18.2,22 12,18.6 5.8,22 7,14.8 2,9.6 8.9,8.6 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 549 B

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<path d="M13.8,4.7l0.7,0.7l-3.4,3.4L7.7,10l-1-1l-1.4,1.4l3.5,3.5l-5.7,5.7L4.6,21l5.7-5.7l3.5,3.5l1.4-1.4l-1-1l1.1-3.4l3.4-3.4
l0.7,0.7L20.9,9l-5.7-5.7L13.8,4.7z M13.7,12l-1,2.9l-3.4-3.4l2.9-1l3.7-3.7l1.4,1.4L13.7,12z"/>
<polygon points="10,3.3 7.8,3.3 7.8,1 6.3,1 6.3,3.3 4,3.3 4,4.8 6.3,4.8 6.3,7 7.8,7 7.8,4.8 10,4.8 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 781 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<path d="M14,11c2.2,0,4-1.8,4-4c0-2.2-1.8-4-4-4s-4,1.8-4,4C10,9.2,11.8,11,14,11z M14,5c1.1,0,2,0.9,2,2c0,1.1-0.9,2-2,2
s-2-0.9-2-2C12,5.9,12.9,5,14,5z"/>
<path d="M16,13h-4c-3.3,0-6,2.7-6,6v2h16v-2C22,15.7,19.3,13,16,13z M8,19c0-2.2,1.8-4,4-4h4c2.2,0,4,1.8,4,4H8z"/>
<polygon points="8,9 5,9 5,6 3,6 3,9 0,9 0,11 3,11 3,14 5,14 5,11 8,11 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 801 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path d="M19.5,5.4c-0.5-0.5-1-1-1.5-1.4c-1.7-1.3-3.8-2-6-2S7.7,2.8,6,4C5.5,4.4,5,4.9,4.5,5.4C2.9,7.2,2,9.5,2,12
c0,2.5,0.9,4.8,2.5,6.6c0.5,0.5,1,1,1.5,1.4c1.7,1.3,3.8,2,6,2s4.3-0.8,6-2c0.5-0.4,1-0.9,1.5-1.4c1.6-1.8,2.5-4.1,2.5-6.6
C22,9.5,21.1,7.2,19.5,5.4z M4,12c0-2,0.8-3.9,2-5.3C7.2,8.1,8,10,8,12c0,2-0.8,3.9-2,5.3C4.8,15.9,4,14,4,12z M12,20
c-1.7,0-3.2-0.5-4.5-1.4C9.1,16.8,10,14.5,10,12c0-2.5-0.9-4.8-2.5-6.6C8.8,4.5,10.3,4,12,4s3.2,0.5,4.5,1.4C14.9,7.2,14,9.5,14,12
c0,2.5,0.9,4.8,2.5,6.6C15.2,19.5,13.7,20,12,20z M18,17.3c-1.2-1.4-2-3.3-2-5.3c0-2,0.8-3.9,2-5.3c1.2,1.4,2,3.3,2,5.3
C20,14,19.2,15.9,18,17.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<path d="M12,22c1.1,0,2-0.9,2-2h-4C10,21.1,10.9,22,12,22z"/>
<path d="M20.1,18.1L20.1,18.1L16,14L9.2,7.2L7.8,5.8L5.9,3.9L4.5,5.3l2.1,2.1C6.2,8.2,6,9.1,6,10v6H4v2h13.2l1.5,1.5L20.1,18.1z
M8,16v-6c0-0.4,0.1-0.7,0.1-1l7,7H8z"/>
<path d="M12,6c2.2,0,4,1.8,4,4v1.2l2,2V10c0-3-2.2-5.4-5-5.9V3h-2v1.1c-0.6,0.1-1.1,0.3-1.6,0.5L11,6.1C11.3,6.1,11.6,6,12,6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 810 B

+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<circle cx="17" cy="8" r="3"/>
<path d="M12,22c1.1,0,2-0.9,2-2h-4C10,21.1,10.9,22,12,22z"/>
<path d="M18,12.9C17.7,13,17.3,13,17,13s-0.7,0-1-0.1V16H8v-6c0-2.2,1.8-4,4-4c0.1,0,0.3,0,0.4,0c0.3-0.7,0.7-1.3,1.3-1.8
c-0.2-0.1-0.5-0.1-0.7-0.2V3h-2v1.1C8.2,4.6,6,7,6,10v6H4v2h16v-2h-2V12.9z"/>
<path d="M6.3,4.3L4.9,2.9C3.1,4.7,2,7.2,2,10h2C4,7.8,4.9,5.8,6.3,4.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 819 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<path d="M12,22c1.1,0,2-0.9,2-2h-4C10,21.1,10.9,22,12,22z"/>
<path d="M18,10c0-3-2.2-5.4-5-5.9V3h-2v1.1C8.2,4.6,6,7,6,10v6H4v2h16v-2h-2V10z M16,16H8v-6c0-2.2,1.8-4,4-4s4,1.8,4,4V16z"/>
<path d="M6.3,4.3L4.9,2.9C3.1,4.7,2,7.2,2,10h2C4,7.8,4.9,5.8,6.3,4.3z"/>
<path d="M19.1,2.9l-1.4,1.4C19.1,5.8,20,7.8,20,10h2C22,7.2,20.9,4.7,19.1,2.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 796 B

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<path d="M12,22c1.1,0,2-0.9,2-2h-4C10,21.1,10.9,22,12,22z"/>
<path d="M18,16v-6c0-3-2.2-5.4-5-5.9V3h-2v1.1C8.2,4.6,6,7,6,10v6H4v2h16v-2H18z M16,16H8v-6c0-2.2,1.8-4,4-4s4,1.8,4,4V16z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 640 B

+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<g>
<g>
<rect x="9" y="8" width="2" height="8"/>
</g>
<g>
<rect x="13" y="8" width="2" height="8"/>
</g>
</g>
<path d="M21,3h-5l-1.4-1.4C14.2,1.2,13.7,1,13.2,1h-2.3c-0.5,0-1,0.2-1.4,0.6L8,3H3v2h2v14c0,1.1,0.9,2,2,2h10c1.1,0,2-0.9,2-2V5h2
V3z M17,19H7V5h10V19z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 735 B

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path d="M12,2c-0.3,0-0.6,0-0.9,0.1c-3.5,0.4-6.4,3.2-7,6.7c-0.5,3.1,0.8,6,3.1,7.7C7.7,16.8,8,17.4,8,18v4h8v-4
c0-0.6,0.3-1.2,0.8-1.6c1.9-1.5,3.2-3.8,3.2-6.4C20,5.6,16.4,2,12,2z M15.6,14.8c-1,0.7-1.6,1.9-1.6,3.2v2h-1.3v-8.1
c0.7-0.3,1.3-1,1.3-1.9c0-1.1-0.9-2-2-2s-2,0.9-2,2c0,0.8,0.5,1.6,1.3,1.9V20H10v-2c0-1.2-0.6-2.4-1.6-3.2C6.6,13.4,5.7,11.3,6.1,9
c0.4-2.6,2.6-4.7,5.2-5c0.2,0,0.5,0,0.7,0c3.3,0,6,2.7,6,6C18,11.9,17.1,13.7,15.6,14.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 886 B

+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<path fill="#221F1F" d="M9,5v4H5V5H9 M9,3H5C3.9,3,3,3.9,3,5v4c0,1.1,0.9,2,2,2h4c1.1,0,2-0.9,2-2V5C11,3.9,10.1,3,9,3L9,3z"/>
<path fill="#221F1F" d="M19,5v4h-4V5H19 M19,3h-4c-1.1,0-2,0.9-2,2v4c0,1.1,0.9,2,2,2h4c1.1,0,2-0.9,2-2V5C21,3.9,20.1,3,19,3L19,3
z"/>
<path fill="#221F1F" d="M9,15v4H5v-4H9 M9,13H5c-1.1,0-2,0.9-2,2v4c0,1.1,0.9,2,2,2h4c1.1,0,2-0.9,2-2v-4C11,13.9,10.1,13,9,13
L9,13z"/>
<path fill="#221F1F" d="M19,15v4h-4v-4H19 M19,13h-4c-1.1,0-2,0.9-2,2v4c0,1.1,0.9,2,2,2h4c1.1,0,2-0.9,2-2v-4
C21,13.9,20.1,13,19,13L19,13z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 995 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<polygon points="9.5,14.1 6,10.6 4.6,12 8.1,15.5 9.5,16.9 19.4,7 18,5.6 "/>
</svg>

After

Width:  |  Height:  |  Size: 520 B

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<polygon points="4.2,9.2 5.6,7.8 12,14.2 18.4,7.8 19.8,9.2 12,17 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 524 B

+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<polygon points="14.8,4.2 16.2,5.6 9.8,12 16.2,18.4 14.8,19.8 7,12 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 526 B

Some files were not shown because too many files have changed in this diff Show More