docs: move the LOTUS_TODO / LOTUS_TESTING backlogs into Gitea issues; keep a reference-only LOTUS_REFERENCE.md
CI / Build & Quality Checks (push) Successful in 1m28s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 6s
CI / Trigger Desktop Build (push) Successful in 6s
CI / Playwright smoke (e2e) (push) Successful in 2m7s
CI / Build & Quality Checks (push) Successful in 1m28s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 6s
CI / Trigger Desktop Build (push) Successful in 6s
CI / Playwright smoke (e2e) (push) Successful in 2m7s
LOTUS_TODO.md → LOTUS_REFERENCE.md (design laws, decided deferrals, server-blocked features, operational reference only). Every open task was filed: cinny #195–#210, cinny-desktop #15–#18, matrix #8–#10. LOTUS_TESTING.md keeps the automated-coverage map, the Playwright notes and the deploy tip; every manual checklist is now a `qa` issue under the 'Manual QA backlog' (cinny #170–#194, #198) and 'Desktop QA backlog' (cinny-desktop #11–#14, #18) milestones. Repointed the README, LOTUS_FEATURES, CI and source comments that referenced LOTUS_TODO.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -239,7 +239,7 @@ jobs:
|
||||
# below are set (create them under repo → Settings → Actions
|
||||
# → Secrets; they are empty until then).
|
||||
# dist/ is rebuilt in-job because actions/upload-artifact@v4 does not work
|
||||
# on this Gitea runner (LOTUS_TODO), so `needs: build` only gates on the
|
||||
# on this Gitea runner (see LOTUS_REFERENCE.md → CI/CD), so `needs: build` only gates on the
|
||||
# main job having passed, not on its artifact.
|
||||
# Hard gate: proven green on the runner in run #1880 (chromium + deps
|
||||
# install fine there). The E2EE tier self-skips without the E2E_* secrets.
|
||||
|
||||
+2
-2
@@ -332,7 +332,7 @@ Users can set a custom background color for `@mention` chips that highlight thei
|
||||
> pre-built npm bundle. Several in-call behaviors below are now first-class
|
||||
> source changes rather than DOM/widget hacks. Background, plan, and the Phase-2
|
||||
> work list are in
|
||||
> the Element Call fork reference in [`LOTUS_TODO.md`](./LOTUS_TODO.md).
|
||||
> the Element Call fork reference in [`LOTUS_REFERENCE.md`](./LOTUS_REFERENCE.md).
|
||||
|
||||
### Element Call — Self-Built Fork (`0.20.1-lotus.1`)
|
||||
|
||||
@@ -1417,7 +1417,7 @@ The session persists as ONE atomic `cinny_session_v1` JSON write (previously ~10
|
||||
|
||||
### Crypto Diagnostics (E2EE investigation kit)
|
||||
|
||||
**Settings → Developer Tools → Crypto Diagnostics**: a capture-only ring buffer (max 200) hooks `console.warn/error` for E2EE failure signatures (OTK upload conflicts, missing call media keys, decryption errors, delayed-event timeouts) and downloads a JSON report — the evidence input for the KE-1→4 investigation. Companion diagnosis: the Encryption / E2EE section of [`LOTUS_TODO.md`](./LOTUS_TODO.md). `utils/cryptoDiagLog.ts`, `features/settings/developer/CryptoDiagnostics.tsx`.
|
||||
**Settings → Developer Tools → Crypto Diagnostics**: a capture-only ring buffer (max 200) hooks `console.warn/error` for E2EE failure signatures (OTK upload conflicts, missing call media keys, decryption errors, delayed-event timeouts) and downloads a JSON report — the evidence input for the KE-1→4 investigation. Companion diagnosis: the Encryption / E2EE section of [`LOTUS_REFERENCE.md`](./LOTUS_REFERENCE.md). `utils/cryptoDiagLog.ts`, `features/settings/developer/CryptoDiagnostics.tsx`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Lotus Chat — Engineering Reference
|
||||
|
||||
**Repo:** `lotus` branch at `https://code.lotusguild.org/LotusGuild/cinny`
|
||||
**Deploy:** push to `lotus` → CI → auto-deploy to `chat.lotusguild.org` (~11 min)
|
||||
|
||||
> **There is no backlog in this file.** All open work lives in Gitea issues — [cinny](https://code.lotusguild.org/LotusGuild/cinny/issues), [element-call](https://code.lotusguild.org/LotusGuild/element-call/issues), [cinny-desktop](https://code.lotusguild.org/LotusGuild/cinny-desktop/issues), [matrix](https://code.lotusguild.org/LotusGuild/matrix/issues) (infra/CI) — grouped by milestone (`Features 2026-Q4`, `Desktop 2026-Q4`, `Manual QA backlog`, `Desktop QA backlog`, the `Audit 2026-09 · *` set). Shipped features are documented in [LOTUS_FEATURES.md](./LOTUS_FEATURES.md); how to run the automated tests is in [LOTUS_TESTING.md](./LOTUS_TESTING.md). The former `LOTUS_TODO.md` backlog was migrated to issues on 2026-09-17 (full history in git).
|
||||
|
||||
This file keeps only what a contributor needs to have open while working: the two design laws, decisions already made, what the server blocks, and the operational reference.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 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. Canonical tokens: `--lt-accent-orange`, `--lt-accent-cyan`, `--lt-accent-green`, `--lt-glow-*`, `--lt-box-glow-*`, `--lt-border-color`, `--lt-font-mono`. Syntax-highlight token classes: `.tok-kw .tok-str .tok-num .tok-cmt .tok-fn`.
|
||||
> Reference patterns: `/root/code/tinker_tickets/` (markdown.js, base.js, ticket.css). Applies to every task without exception.
|
||||
> New components must respect both TDS dark (`LotusTerminalTheme`) and TDS light (`LotusTerminalLightTheme`); non-TDS theme work uses vanilla-extract (match `src/lotus-terminal.css.ts`).
|
||||
|
||||
## 🧩 NATIVE-CINNY LAW — EVERY FEATURE MUST FEEL LIKE STOCK CINNY
|
||||
|
||||
> **Every feature must feel native to upstream Cinny — indistinguishable from what the Cinny team would ship.** Reference: <https://github.com/cinnyapp/cinny>.
|
||||
>
|
||||
> - **Use the `folds` design system, not bespoke UI** (`Button`, `Chip`, `IconButton`, `Menu`, `MenuItem`, `Dialog`, `Modal`, `Input`, `Switch`, `Badge`, `SettingTile`, `SequenceCard`, …) and folds tokens (`color.*`, `config.space.*`, `config.radii.*`). **Use folds `Icon`/`Icons`, never literal emoji, in UI chrome.** No hardcoded hex/`rgba()`, no invented CSS variables.
|
||||
> - **Match Cinny's existing patterns** — find the closest existing component/flow and mirror it before adding UI.
|
||||
> - **The ONE exception:** explicit **TDS** features, which follow the TDS Design Law above (opt-in, only in Lotus Terminal mode).
|
||||
|
||||
---
|
||||
|
||||
## Deferred / dropped (decided — kept for context)
|
||||
|
||||
- **[DEFERRED] P5-51** Federated "Identity Contexts" (session isolation) — multi-sprint, touches auth/crypto/storage core; smaller intermediate step = plain multi-account switch. **[DROPPED] P5-52** per-room sync governor — js-sdk can't truly per-room filter `/sync`; only a cosmetic hide. **[DEFERRED] P5-53** local scripting plugin — prefer a declarative automation-rules feature (no arbitrary code). **[DEFERRED] Audit-3** profile banner — MSC4427 open/unmerged; revisit on merge. **[WON'T FIX] P5-50** Windows HW media pipeline (WebRTC decode lives in WebView2; not injectable). **[MOVED] P5-9** LFG → LotusBot `!lfg`.
|
||||
|
||||
---
|
||||
|
||||
## 🚫 Blocked Features (server / upstream gated)
|
||||
|
||||
Re-run `/_matrix/client/versions` + `unstable_features` after each Synapse upgrade. **Re-checked on 1.157.1 (2026-07-23): no change — all four below are still `false`.** The 1.156.0→1.157.1 delta unblocked nothing (it's a bugfix release; the only feature-bearing release in the gap was 1.156.0, which we were already running).
|
||||
|
||||
- **[BLOCKED] Live Location Sharing** (MSC3489 + MSC3672 both `false`) — real-time GPS beacons over the existing static share.
|
||||
- **[BLOCKED] Reaction/Relation Redaction** (MSC3892 `false`) — remove a reaction without redacting the parent; current full-redaction fallback is acceptable.
|
||||
- **[DONE 2026-07] Room Preview before joining** (MSC3266) — the client was always built (`JoinBeforeNavigate` → `RoomCard` via `mx.getRoomSummary`). The earlier "blocked" flag was a **misdiagnosis**: it tested `/v1/rooms/{id}/summary` (404), but the SDK calls the _unstable_ `im.nheko.summary/summary/{id}` path, which returns **200** with name/topic/members/join_rule. Verified live after the 1.156 upgrade; also added a join-rule/encryption chip + Request-to-join for knock rooms to the preview card.
|
||||
- **[BLOCKED] Thread Subscriptions** (MSC4306 `false`) — "Follow thread" button (depends on the shipped Thread Panel).
|
||||
|
||||
---
|
||||
|
||||
## 📖 Reference
|
||||
|
||||
### Server Capabilities (as of 2026-07)
|
||||
|
||||
- **Homeserver** `matrix.lotusguild.org` · **Synapse** `1.157.1+trixie1` (upgraded 2026-07-23 from **1.156.0** — note the host was found on 1.156.0 while the docs claimed 1.155.0, so **always verify with `dpkg-query -W matrix-synapse-py3`**, don't trust the docs; apt package on Debian 13, LXC 151) · **Matrix spec** up to `v1.12` (Synapse still advertises v1.12; MSC features via `unstable_features`).
|
||||
- **MSC ON** (re-dumped live from `/_matrix/client/versions` on 1.157.1): `msc4140` · `msc3771` · `msc3440.stable` · `msc4133.stable` · `simplified_msc3575` · `msc4222` · `msc3266` (room summary live at unstable `im.nheko.summary/summary/{id}` — 200; the `/v1/rooms/{id}/summary` path is still 404) · `msc3401_matrix_rtc` · `msc2285.stable` · `msc3827.stable` · `msc3981` · `msc4380.stable` · `msc4445` · `msc2659.stable` · `msc2666` · `msc2432` · `e2e_cross_signing` · `label_based_filtering`. **OFF/blocked:** `msc4306` · `msc3882` · `msc3912` · `msc4155` · `msc3489`/`msc3672` · `msc3892` · `msc4028` · `msc4069` · `msc4108` · `msc3391` · `msc4354` (sticky events — **deliberately off**, see the Matrix 2.0 section above) · `msc4143` (RTC foci — **not a gap**: LiveKit is discovered via `.well-known` `org.matrix.msc4143.rtc_foci`, confirmed live, not this flag).
|
||||
- **Dead client code:** Synapse 1.157.0 **removed** `msc3861` (MAS auth delegation) entirely — the ~6 `msc3861`/`msc2965` references in `src/` can never activate against this homeserver (we auth via Authelia `oidc_providers`). Harmless, but cleanup material.
|
||||
- **Live endpoints:** Report User (MSC4260) **200** ✅ · Report Room (MSC4151) ✅.
|
||||
- **Homeserver access (audits):** Synapse = LXC 151 (`pct exec 151 -- bash`), config `/etc/matrix-synapse/homeserver.yaml`. Web deploy = LXC 106. Voice guard = `voice-limit-guard.py` on LXC 151.
|
||||
- **SDK notes:** no arbitrary profile-field methods (use `mx.http.authedRequest()` for MSC4133); js-sdk can't per-room filter `/sync`; sanitizer strips `<math>`/MathML; SW exists at `src/sw.ts`; `getMatrixToRoom()` builds invite URLs; EC audio-inject unblocked via the fork's `io.lotus.inject_audio`.
|
||||
|
||||
### Key File Reference
|
||||
|
||||
| What | File | Lines |
|
||||
| ------------------------------ | ------------------------------------------------------------------- | ------------------- |
|
||||
| Global keydown / room nav | `hooks/useKeyDown.ts` · `hooks/useRoomNavigate.ts` | whole / 19-72 |
|
||||
| Room unread counts atom | `state/room/roomToUnread.ts` | `roomToUnreadAtom` |
|
||||
| Overlay portal provider | `pages/App.tsx` · `index.html` | 65 / 101 |
|
||||
| Room settings tabs | `features/room-settings/RoomSettings.tsx` | 27-56 |
|
||||
| State event read/write pattern | `features/common-settings/general/RoomEncryption.tsx` | 42-52 |
|
||||
| Power levels | `hooks/usePowerLevels.ts` | whole |
|
||||
| Slash commands | `hooks/useCommands.ts` | 140-537 |
|
||||
| Chat background picker/defs | `features/settings/general/General.tsx` · `lotus/chatBackground.ts` | 945-981 / whole |
|
||||
| Matrix.to URL builder | `plugins/matrix-to.ts` | `getMatrixToRoom()` |
|
||||
| Media URL conversion | `utils/matrix.ts` | `mxcUrlToHttp()` |
|
||||
| Search pagination / virtual | `features/message-search/{useMessageSearch,MessageSearch}.tsx` | 74-121 / 234-365 |
|
||||
| Call mic control | `plugins/call/CallControl.ts` | 206-212 |
|
||||
| Knock support check | `utils/matrix.ts` | 376-391 |
|
||||
| Notification mute push rules | `hooks/useRoomsNotificationPreferences.ts` | 110-150 |
|
||||
|
||||
### Element Call fork — operational reference
|
||||
|
||||
Fork = `LotusGuild/element-call` (branch `lotus`, upstream base **v0.25.0** since the 2026-09 sync — was v0.20.1); cinny consumes the npm package `@lotusguild/element-call-embedded` (built bundle copied into `public/element-call/`).
|
||||
|
||||
**Toolchain (upstream-driven, accepted 2026-09):** Node ≥ 22.13 (`.node-version` = 24) and **pnpm 11**, installed directly (`npm i -g pnpm@<packageManager version>`, currently 11.21.0) — **not** via `corepack enable`: `matrix-js-sdk` is a git dependency pnpm builds from source, and its own devEngines pins pnpm 11.9.0; a corepack-shimmed pnpm refuses to switch for that nested install and `pnpm install` fails (fork CI run #1854). pnpm 10 rejects the lockfile and Node 20 cannot build. Lint is **oxlint + oxfmt** (upstream dropped eslint/prettier in v0.25.0): `pnpm lint` (tsc + oxlint + knip) and `pnpm format:check` / `pnpm format`. `matrix-js-sdk` is pinned to a `matrix-org/matrix-js-sdk#develop` commit in the lockfile, as upstream ships it. Fork CI (`.gitea/workflows/ci.yml`) hard-gates lint + format + `pnpm test:unit` before build, with `concurrency: cancel-in-progress`.
|
||||
|
||||
**Publish a new version (CI on tag push; needs the `NPM_PUBLISH_TOKEN` org secret):** the published version is derived from the git tag — bump `embedded/web/package.json` (currently `0.25.0-lotus.3`, published by CI; the secret is `NPM_PUBLISH_TOKEN`, names starting `GITEA_` are reserved), push `lotus`, then `git push lotus v0.25.0-lotus.1`; the `publish` job builds and publishes to the Gitea registry. Always push (never delete) the annotated `vX.Y.Z-lotus.N` tag for every published version. Then in cinny bump the `@lotusguild/element-call-embedded` pin (currently `0.25.0-lotus.3`) → `npm install` → build. Manual fallback: `pnpm run build:embedded && cd embedded/web && npm version <ver> --no-git-tag-version && npm publish`.
|
||||
|
||||
**`io.lotus.*` widget actions** (add new toWidget actions to the enum + `LOTUS_TO_WIDGET_ACTIONS` in `src/lotus/lotusActions.ts`; only send AFTER call-join or a 10s timeout fires):
|
||||
|
||||
| Action | Dir | Purpose | Module |
|
||||
| :--------------------------- | :------ | :----------------------------------------------------- | :-------------------- |
|
||||
| `io.lotus.call_state` | EC→host | speaker/mute/camera stream (`lotusCallState=1`) | `lotusCallState.ts` |
|
||||
| `io.lotus.focus_participant` | host→EC | spotlight (works during screenshare) | `lotusFocus.ts` |
|
||||
| `io.lotus.inject_audio` | host→EC | soundboard clip mixed into call (`lotusAudioInject=1`) | `lotusAudioInject.ts` |
|
||||
| `io.lotus.set_quality` | host→EC | audio/screenshare bitrate/fps caps | `lotusQuality.ts` |
|
||||
| `io.lotus.decorations` | host→EC | in-call avatar decorations | `lotusDecorations.ts` |
|
||||
| `io.lotus.set_deafen` | host→EC | LiveKit-source deafen (P6-2) | `lotusDeafen.ts` |
|
||||
|
||||
Also flag-gated: `lotusTransparent`/`lotusTheme`, `lotusDenoiseSource=1` (in-source ML denoise).
|
||||
|
||||
### CI/CD + per-feature checklist
|
||||
|
||||
```
|
||||
edit → commit → git push origin lotus
|
||||
→ Gitea Actions (.gitea/workflows/ci.yml): npm ci → build + npm test + tsc + eslint + prettier (ALL hard gates) → audit + bundle-size (informational)
|
||||
→ lotus_deploy.sh on LXC 106 polls the "Build & Quality Checks" status → npm ci && npm run build → rsync → live (~11 min)
|
||||
(a push that lands while a deploy is mid-build is queued and deployed right after — matrix@b6ea4a3; before that it was dropped)
|
||||
```
|
||||
|
||||
Before marking a feature complete: `npx tsc --noEmit` (0 errors) · `npx eslint src/` (0 new) · `npx prettier --check src/` · `npm test` (Node runner via tsx, hard CI gate — colocated `*.test.ts`) · update `README.md`/`landing/index.html` for Lotus-custom features · visually verify on `chat.lotusguild.org`.
|
||||
|
||||
**CI hardening (2026-07, reviewed):**
|
||||
|
||||
- [x] **Concurrency** — `cancel-in-progress` on cinny `ci.yml` and cinny-desktop `release.yml` (`386a2979` / `c5461ce`): a superseded lotus push cancels its in-flight web CI and collapses queued ~30-min Tauri desktop builds to just the newest. Safe for deploys because `lotus_deploy.sh` now **follows origin/lotus HEAD** each poll iteration + resets to the gated SHA (`matrix` `c15a489`) — closes the latched-SHA freeze race.
|
||||
- [x] **Hard quality gates** — typecheck/eslint/prettier promoted from `continue-on-error` to blocking (tree held clean). eslint gates on errors only; `no-explicit-any` warnings stay informational.
|
||||
|
||||
**CI follow-ups (open):**
|
||||
|
||||
**CI follow-ups** are tracked in the `matrix` repo: [#8](https://code.lotusguild.org/LotusGuild/matrix/issues/8) dedicated `desktop-linux` runner, [#9](https://code.lotusguild.org/LotusGuild/matrix/issues/9) debounce the desktop trigger, [#10](https://code.lotusguild.org/LotusGuild/matrix/issues/10) verify Gitea `concurrency`. Build-once/deploy-the-artifact was considered and deferred (noted on #8).
|
||||
+8
-801
@@ -1,38 +1,21 @@
|
||||
# Lotus Chat — Manual Testing Guide
|
||||
# Lotus Chat — Testing
|
||||
|
||||
**Generated:** June 2026 · **Updated:** July 2026 (added §O — threads, per-thread notifications, math, search cache, session hardening, audit wave, desktop CSP; added the **Automated coverage map** below — logic now pinned by unit tests, so manual QA can focus on the human-only surface)
|
||||
**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.
|
||||
> **Manual QA checklists no longer live here.** Every shipped-but-unverified behaviour is a Gitea issue labelled **`qa`** — cinny milestone [Manual QA backlog](https://code.lotusguild.org/LotusGuild/cinny/milestones), cinny-desktop milestone [Desktop QA backlog](https://code.lotusguild.org/LotusGuild/cinny-desktop/milestones). Each issue carries the full steps + expected results; tick items as they pass, comment on FAIL (what you saw vs expected, browser/OS, web vs desktop, theme, console errors), close when green. Migrated from this file on 2026-09-17 (full text in git history).
|
||||
|
||||
> **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.
|
||||
This file keeps what a contributor needs to run and extend the **automated** coverage, plus the ops tip at the bottom.
|
||||
|
||||
## 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) |
|
||||
- Test the **web** build (`chat.lotusguild.org`, LXC 106) first; re-run call + poll items on the **desktop (Tauri)** build too, since CSP and the EC iframe behave differently there.
|
||||
- Several call checks need a **second participant** (marked **👥 2 people** in the issues); a couple need a third room/call in parallel (**👥👥**).
|
||||
|
||||
---
|
||||
|
||||
## Automated coverage map — what the unit tests already pin (2026-07)
|
||||
|
||||
**Read this before working the guide.** Much of the _logic_ these manual checks were written to catch is now locked by deterministic unit tests (`npm test`, 920+ cases, green in CI). Unit tests do **not** prove visual rendering, real-call behavior, the desktop build, E2EE, or cross-device sync — those still need a human. But where a decision is pure logic, you can **trust the test and spend your manual time on the human-only part**. For each row below, the middle column is "don't bother re-deriving this by hand"; the right column is "this is what your manual pass is actually for."
|
||||
**Read this before working a `qa` issue.** Much of the _logic_ the manual checks were written to catch is now locked by deterministic unit tests (`npm test`, 920+ cases, green in CI). Unit tests do **not** prove visual rendering, real-call behavior, the desktop build, E2EE, or cross-device sync — those still need a human. But where a decision is pure logic, you can **trust the test and spend your manual time on the human-only part**. For each row below, the middle column is "don't bother re-deriving this by hand"; the right column is "this is what your manual pass is actually for."
|
||||
|
||||
| Guide item | Logic **pinned by a unit test** (trust it) | What still needs **you** (manual) |
|
||||
| QA item | Logic **pinned by a unit test** (trust it) | What still needs **you** (manual) |
|
||||
| :----------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **A1** ringtone previews | `callSounds.test.ts` — each style's synthesized melody (chime/soft/retro), click-free gain ramps, context unlock/reuse, unknown = no-op | that it's actually **audible** + the WebAudio first-gesture caveat |
|
||||
| **A2** ringtone persist/fallback | `settings.test.ts` — unknown `ringtoneId` → default, malformed JSON → defaults, merge-over-defaults (this **is** A2 step 3) | the dropdown shows the persisted value after reload (trivial glance) |
|
||||
@@ -48,7 +31,7 @@
|
||||
| **Q1/Q2** embeds (URL→player) | `videoEmbed.test.ts` (26) — every provider's URL→`{provider, kind, embedUrl, height}` parse (incl. Mixcloud/Deezer, TikTok, reserved-path guards) | the click-to-play **facade**, no-network-until-Play, the **CSP** (esp. desktop), visuals |
|
||||
| **Seasonal theme resolution** (part of F2) | `seasonSchedule.test.ts` — `resolveSeasonTheme` (off→none, auto→active season, pinned→that) + `getActiveSeason` priority/boundary days. **NB: this pins _which_ theme shows for a date, NOT F2's background↔seasonal mutual exclusion** — that write-side logic is untested | all of **F2**: the picker actually clearing the _other_ setting live, and the overlay suppression when a background is set |
|
||||
|
||||
Everything else in the guide (calls, screen readers, desktop/Tauri, chat backgrounds, animated visuals, PWA install, real E2EE) is genuinely manual — no unit test substitutes for it. Items already **verified live** are listed at the very bottom ("Verified working in live testing").
|
||||
Everything else in the guide (calls, screen readers, desktop/Tauri, chat backgrounds, animated visuals, PWA install, real E2EE) is genuinely manual — no unit test substitutes for it. Items already verified live were dropped when the checklists moved to issues.
|
||||
|
||||
---
|
||||
|
||||
@@ -70,782 +53,6 @@ The `e2e` job is `continue-on-error: true` for now because `playwright install -
|
||||
|
||||
---
|
||||
|
||||
## 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 ~3–5 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 A–D above are **this session's** work. Everything below was fixed in earlier waves and is still flagged **⚠️ UNTESTED** (see the outstanding-verification backlog below / `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 A–D 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.
|
||||
|
||||
---
|
||||
|
||||
## O. July 2026 batch — threads, notifications, math, search cache, audit wave
|
||||
|
||||
Everything landed after the OIDC work. These mirror the checklists in `LOTUS_TODO.md` (§P3-8, §P4-1) and the outstanding-verification backlog below (P3-8/P4-1/P4-4/P4-8/N97a/AW-1…4). **⚠️ Threads change the main timeline** — thread replies no longer render inline; that's intended (see O1).
|
||||
|
||||
### O1. Thread Panel (P3-8) — 👥 2 people help for live replies
|
||||
|
||||
1. Hover a message → **Reply in Thread** (message menu). The right-side **thread panel** opens with that message as the root.
|
||||
2. Send text, an emoji, and a file upload into the thread; have the second person reply too.
|
||||
3. Reply to a reply _inside_ the panel.
|
||||
|
||||
**Expected:** the panel shows the root at top + an "N replies" divider + the reply timeline (own composer at the bottom). Your sends appear immediately (pending → confirmed). A reply-to-a-reply is a proper thread reply. In the **main** timeline the replies do **not** appear inline — the root message instead shows a **"N replies · time"** chip. Clicking the chip (or a reply's thread indicator) opens the panel. **×** or **Escape** closes it; on mobile the panel is fullscreen. Scrolled up in a long thread → a **Jump to Latest** chip appears. Reload the page → the root/reply split persists; in an **encrypted** room the thread replies decrypt (not "Unable to decrypt").
|
||||
|
||||
### O2. Per-thread notifications (P4-1, Slack-style) — 👥 2 people
|
||||
|
||||
1. Have the second person reply in a thread **you have posted in** → expect a notification + sound.
|
||||
2. Have them reply in a thread **you have never touched** and don't @mention you → expect **silence** (only the chip's unread badge updates).
|
||||
3. Have them **@mention** you in any thread → expect a notification regardless of participation.
|
||||
4. Open the panel's **bell menu** (header) → set the thread to **Mute** → expect no notifications, the chip's unread badge gone (bell-mute glyph shown), and the room's **sidebar badge drops** by that thread's count. Try **All** (every reply notifies) and **Mentions only** (only @mentions).
|
||||
5. On a **second device**, confirm the same per-thread modes are set (they sync via account data).
|
||||
6. Room-level **Mute** (room context menu) still silences everything, including thread overrides.
|
||||
|
||||
**Known caveat:** Mentions-only can under-notify in E2EE rooms (the decision runs before decryption). Muted-thread badge subtraction is Lotus-only.
|
||||
|
||||
### O3. Math / LaTeX (P4-4)
|
||||
|
||||
Send each and confirm rendering: `$x^2 + y^2$` (inline), `$$\int_0^1 f(x)\,dx$$` (block, centered), `$5 and $10 for lunch` (**stays plain text** — currency guard), and a code block containing `$x$` (**stays literal** inside the code block). **Expected:** the first two render as math (KaTeX); the last two are untouched. First math of the session may show the raw `$…$` for a beat while the KaTeX chunk lazy-loads, then renders.
|
||||
|
||||
### O4. Encrypted search cache (P4-8) — opt-in
|
||||
|
||||
In an **encrypted** room's message search, enable **"Persist search index on this device"** (Encrypted Rooms panel). Search, then **reload** and search the same term. **Expected:** coverage survives the reload (results without re-paginating everything). **Clear cached index** empties it. **Log out** → the cache is wiped (privacy). Toggling the setting OFF does **not** wipe (only Clear/logout do).
|
||||
|
||||
### O5. Session hardening (N97a) — cross-tab
|
||||
|
||||
1. Log in on a build that predates the change, then load this build → you stay logged in (legacy keys migrate to the `cinny_session_v1` blob; check DevTools → Application → Local Storage).
|
||||
2. Open the app in **two tabs**; **log out** in tab A → tab B reloads to the auth screen within a moment. Log in again in one tab → the other reloads too.
|
||||
|
||||
### O6. Audit-wave correctness fixes (AW-1)
|
||||
|
||||
- **Scheduled-message cancel:** schedule a message, then cancel it **with the network cut** (DevTools offline) → the item **stays** with an inline error (it does **not** silently disappear and still send). Restore network, retry → cancels cleanly.
|
||||
- **Escape coordination:** in a thread panel, open the mention autocomplete or set a reply draft, press **Escape** → it dismisses the autocomplete/reply **without** closing the panel. A bare Escape (nothing to dismiss) still marks the room read / closes the panel as before.
|
||||
- **Panel exclusivity:** on mobile, opening a thread while the media gallery (or members drawer) is open shows only **one** right panel (thread wins), not stacked fullscreen overlays.
|
||||
- **Emoji board (AW-2):** the **first** time you open the emoji board / autocomplete in a session, the grid **and search** populate with unicode emoji (they don't stay empty). Reactions still show a label.
|
||||
|
||||
### O7. Desktop (Tauri) — CSP tighten + native stack (AW-4) — 🖥️ desktop build only
|
||||
|
||||
The webview CSP was tightened and the full native module set now compiles. Smoke-test the desktop build:
|
||||
|
||||
1. App **boots**, avatars + media thumbnails load, the **VT323** terminal font renders (Lotus Terminal theme), a **location message** embeds its OpenStreetMap map, **calls** connect (EC iframe), **deep links** (`matrix:` / clicking a room link) navigate.
|
||||
2. **Native features:** minimize to tray (notifications still arrive), a message notification is a **rich toast** (click opens the room; reply box sends), the taskbar **Jump List** lists recent rooms, in a call the taskbar thumbnail shows **Mute/Deafen/End**, Windows **Focus Assist** silences Lotus.
|
||||
3. **Console** (desktop devtools) shows **no CSP violations** during normal use. If something visual/media is blocked, that's the CSP to loosen — note exactly what and where.
|
||||
|
||||
### O8. E2EE / call-key cluster (KE-1→4) — 👥 2 people, during a real call
|
||||
|
||||
We shipped the diagnostics kit + a **Crypto Diagnostics** card (**Settings → Developer Tools**). During your next call that glitches (audio cutouts, "Unable to decrypt"), open it and **Download report**, and note whether the symptoms even still occur now that we're on **matrix-js-sdk 41.7.0** (crypto-wasm 18.3.1). Send me the report; the KE-1..4 diagnosis + capture guidance is in `LOTUS_TODO.md` (Encryption / E2EE), with the full original runbook in git history.
|
||||
|
||||
---
|
||||
|
||||
## P. Accessibility (P3-4) — needs a browser + a screen reader
|
||||
|
||||
The compliance fixes are gate-verified in code; these confirm the runtime a11y behavior only a human + AT can check. Tools: browser DevTools "axe" extension / Lighthouse a11y, plus **VoiceOver** (macOS ⌘F5) or **NVDA** (Windows).
|
||||
|
||||
### P1. Keyboard-only golden path (no mouse)
|
||||
|
||||
Tab from page load: **skip-to-content** link appears first (Enter jumps to the timeline). Tab reaches the room list (rooms are focusable, active room announced), open a room (Enter), type a character → focus lands in the composer, send with Enter (or Shift+Enter per your `enterForNewline` setting). No keyboard trap; visible focus ring throughout.
|
||||
|
||||
### P2. `?` shortcuts dialog
|
||||
|
||||
Press **?** (Shift+/) with focus NOT in a text field → the keyboard-shortcuts dialog opens, is focus-trapped, Escape closes it and focus returns to where you were. Pressing `?` while typing in the composer/search inserts a literal `?` (does NOT open the dialog).
|
||||
|
||||
### P3. Screen-reader: reading messages
|
||||
|
||||
With VoiceOver/NVDA on, arrow through the timeline: each message is announced as an article with **sender name + time** — critically, this includes **collapsed messages** (consecutive messages from the same person), which previously announced only the body with no sender. Reactions, "edited", replies, and delivery status are announced with labels.
|
||||
|
||||
### P4. Screen-reader: live announcements
|
||||
|
||||
- **New message** arrives while you're reading → announced (polite).
|
||||
- **Someone starts typing** → "X is typing" announced once (not spammed per keystroke).
|
||||
- **Editing a message** → the edit box announces "Editing message from X".
|
||||
|
||||
### P5. Focus return from dialogs
|
||||
|
||||
Open then close (Escape or ×): the **room topic viewer**, a **reaction viewer** (click a reaction count), and **Search** → focus returns to the button/element you opened them from (not lost to `<body>`). Inline popouts (emoji picker, autocomplete, hover menus) intentionally keep focus in context — that's expected, not a bug.
|
||||
|
||||
### P6. axe / Lighthouse scan
|
||||
|
||||
Run the axe DevTools extension (or Lighthouse → Accessibility) on a room view, Settings, and the login screen. Expect **no critical/serious** "missing accessible name" or "ARIA" violations on the golden path. Report any that appear (note: far-scrolled timeline history being virtualized out is a known, accepted limitation — not a finding).
|
||||
|
||||
---
|
||||
|
||||
## Q. Inline Media Embeds — video / audio / post players (needs the web deploy live)
|
||||
|
||||
The whole feature is behind **Settings → General → "Inline Media Players"** (default **on**). Everything loads from the homeserver's cached thumbnail first; the third-party player only mounts on **Play**. Test on the **web** build first, then re-check the video ones on **desktop (Tauri)** since the CSP differs. On any failure, grab the **browser console** (F12) — a blocked embed shows as a CSP `frame-src` violation naming the host.
|
||||
|
||||
### Q1. Facade + one of each kind plays in place
|
||||
|
||||
Paste each of these into a room and confirm a media tile (not a plain link) with a thumbnail + play button, and that clicking Play mounts the player **inline**:
|
||||
|
||||
- **16:9 video:** a YouTube `watch` link, a Vimeo link, a Dailymotion link, a Streamable link, a Twitch VOD/clip, a Loom `share` link.
|
||||
- **9:16 portrait:** a YouTube **Shorts** link (renders tall, not letterboxed).
|
||||
- **Audio player:** a Spotify track, a SoundCloud track, an Apple Music album, a Tidal album/track.
|
||||
- **Post embed:** an X/Twitter post, an Instagram post, a Reddit post.
|
||||
|
||||
**Expected:** ✅ tile shows the thumbnail; **no** request to the third party until you press Play (check DevTools → Network); the player then plays inline. ❌ tell me any that stay a plain link, show a blank frame, or hit the network before you click.
|
||||
|
||||
### Q2. TikTok (the tricky one) + portrait fill
|
||||
|
||||
1. Paste a **full** TikTok URL and a **short** copy-link (`vm.tiktok.com/…` or `tiktok.com/t/…`).
|
||||
2. Press Play on each.
|
||||
|
||||
**Expected:** both resolve to a clean **9:16** player that **fills the box** (no big empty band on the right). The short link shows a brief spinner while it resolves via oEmbed, then plays. ❌ tell me if a short link shows only the TikTok logo/♫ and never a play button, or if the player has dead space beside it.
|
||||
|
||||
### Q3. Post self-resize + Close / Fullscreen controls
|
||||
|
||||
1. Play a **Reddit**, **Instagram**, and **X/Twitter** post embed.
|
||||
2. Watch the card height as the embed loads.
|
||||
|
||||
**Expected:** the card **grows to fit** the post (no clipped/scrollbarless content, no giant empty box). A **Close** button (✕) collapses the player back to the thumbnail; video players also show a **⛶ Fullscreen** control that works. Keyboard: Tab to the play button → it shows a visible **focus ring**.
|
||||
|
||||
### Q4. New providers (unverified) + the toggle + the cap
|
||||
|
||||
- **Bluesky / Loom / Kick** — these are freshly added and unverified live. Paste a `bsky.app/profile/…/post/…`, a `loom.com/share/…`, and a live `kick.com/{channel}` link. ✅ good if each plays/renders inline; ❌ if any is a broken frame (for **Bluesky** especially, note whether a **handle** URL resolves or only a DID one does — grab the console).
|
||||
- **Toggle off:** Settings → General → **Inline Media Players** off → every media link reverts to a plain link tile (no player).
|
||||
- **Cap:** paste a message with **8+** media links → at most **6** preview cards render (the rest are suppressed), and the page stays responsive.
|
||||
|
||||
---
|
||||
|
||||
## R. Discovery-pass fixes (DP1–DP18, 2026-07)
|
||||
|
||||
Agent-surveyed + TPVR-verified low/med issues, now fixed (commits `8eb961b6` `db864326` `6cf18c3b` `165714e1` `8c0e2b42` `e545706c` `b1ee3ada` `4fc3f7a3` `101e4116` `4fa4327a` `c2598d21`). Behavioral items have concrete checks; the refactors just need a "still works" regression pass.
|
||||
|
||||
### Correctness
|
||||
|
||||
- [ ] **DP1 — slash-command errors are visible.** Run a slash command that must fail — e.g. `/kick @nobody:server` in a room where you lack permission, or `/join` a bad alias. **Expected:** an error toast appears (not a silent no-op); a successful command still clears the composer normally.
|
||||
- [ ] **DP2 — no invite re-notify on reload.** With ≥1 pending invite, hard-reload (Ctrl+F5). **Expected:** NO "you have N new invitation" toast/sound on load. Then have someone invite you while the app is open → you DO get one notification for the new invite. (👥 2 accounts)
|
||||
- [ ] **DP3 — status clear syncs across devices.** Set a status message on device A, confirm it shows on B; clear it on A. **Expected:** B clears too and does NOT re-publish the old status on its next presence heartbeat. Toggling Invisible must not wipe a real saved status. (👥 2 sessions)
|
||||
- [ ] **DP4 — tag-toggle failure surfaced once.** With the network offline, toggle a room's Favourite/Low-priority. **Expected:** a single error toast (not two) on failure; on success the tag updates as before.
|
||||
- [ ] **DP5 — soundboard packs update on room switch.** Open a soundboard in room A, then switch to room B (different pack) in the same mounted view. **Expected:** B's packs show without needing an unrelated event.
|
||||
- [ ] **DP6 — declining a call still dismisses.** Decline an incoming call. **Expected:** the ringing UI dismisses even if the decline send fails (best-effort). (👥 2 accounts)
|
||||
|
||||
### a11y / UX (needs a screen reader + a narrow viewport)
|
||||
|
||||
- [ ] **DP7 / DP8 — call-control buttons announce correctly.** In a call with a screen reader: the deafen button announces "Deafen" when sound is on (not "Undeafen"); Sound / Video / Screenshare announce a consistent pressed/unpressed state like Mic.
|
||||
- [ ] **DP9 — GIF picker focus + width.** Open the GIF picker, close it (Esc / click-out) → focus returns to the GIF button. On a ~320px viewport the picker doesn't overflow the page.
|
||||
- [ ] **DP10 — search-filter clears by keyboard.** In message search, toggle a filter chip (Has link / msg-type / pinned) off with Enter; the date-range clears via its menu's Clear. No mouse-only clear needed.
|
||||
- [ ] **DP11 — voice recorder fits + announces.** On a ~360px viewport, start a voice message → the recorder row doesn't overflow the composer; a screen reader can query the duration (role="timer") without being spammed.
|
||||
- [ ] **DP12 — live-call count announced.** With a screen reader, when someone joins/leaves an active call, the "{n} Live" change is announced (polite status region).
|
||||
|
||||
### TDS colors (Lotus Terminal theme)
|
||||
|
||||
- [ ] **DP14 — send-status + receipt colors follow the theme.** In **TDS light** mode: the message send-status "failed" icon and the read-receipt pill use the theme's darker red/blue (from `--lt-*` tokens), NOT bright dark-mode cyan/red. TDS dark still looks right; non-TDS themes unchanged.
|
||||
|
||||
### Refactor regression pass (no behavior change intended)
|
||||
|
||||
- [ ] **DP13 — bookmarks / reminders / notes still work** (they now share one store engine). Add/remove a bookmark, set/clear a reminder, write/clear a user note; each persists across reload; rapid consecutive writes don't clobber each other.
|
||||
- [ ] **DP15 / DP16 — state-event + account-data writes still work.** Edit room name/topic/avatar, join-rules, power levels, an emoji/soundboard pack (state events); toggle a setting stored in account data. All save + reflect correctly.
|
||||
- [ ] **DP17 — link previews render.** Open a TikTok / Spotify / Steam / Reddit link preview; provider icons render (folds icons, not raw glyphs) and brand colors look right.
|
||||
- [ ] **DP18 — member avatars/names live-update.** Read receipts + the "seen by" reader list show correct avatars/names and update live when a member changes their avatar or display name (no reload).
|
||||
|
||||
---
|
||||
|
||||
## Priority if you're short on time
|
||||
|
||||
1. **O1 + O2** (threads + per-thread notifications) — the largest new surface; the main-timeline change is user-visible.
|
||||
2. **O7** (desktop CSP smoke) — CI can't catch CSP breakage; a wrong directive silently breaks media/fonts/maps.
|
||||
3. **O5** (session cross-tab) + **O6** (scheduled-cancel ghost-send) — auth-critical + a real data-loss-class fix.
|
||||
4. **A4** (in-call banner) + **A3** (ringtone) — newest call logic, hardest to reproduce.
|
||||
5. **D** (EC control sweep) — guards against the fork breaking calls.
|
||||
6. Everything else.
|
||||
|
||||
---
|
||||
|
||||
## Outstanding verification backlog
|
||||
|
||||
**Room Widgets (MSC1236, 2026-07 — needs the CSP `frame-src` widening + `nginx -s reload` first):** In a room, the header **Widgets** button (grid icon, desktop) opens a right-side panel. As an admin (PL to modify widgets): **Add Widget** with a name + an https URL (e.g. an Etherpad `https://…` or any embeddable page) → it appears in the list; click it → it renders in a sandboxed iframe in the panel; **Remove** clears it. A non-admin sees the list + can open widgets but has no Add/Remove. Check: a non-https or same-origin URL is rejected on Add with a clear message; the panel is a full-screen overlay on mobile and is mutually exclusive with the Thread/Gallery/Members panels; if a widget stays blank, the prod CSP `frame-src` still needs widening. Widgets get only benign display capabilities (they can't send/read room events in v1).
|
||||
|
||||
**QR Device Verification (2026-07):** With two logged-in Lotus sessions (or Lotus + Element), start a device verification. On the **Ready** step you now see your own QR code plus a **"Scan their QR code"** button and a **"Verify with emoji instead"** fallback. Have one device **scan** the other's code (grant camera permission) → the showing device asks you to **Confirm**, and both reach **verified**. Check: emoji-SAS still works unchanged; denying camera shows a graceful "verify with emojis instead" message; a deliberately-wrong scan cancels cleanly. Desktop (WebView2) auto-grants the camera; web needs the Permissions-Policy camera allowance (already set).
|
||||
|
||||
**Disappearing Messages (MSC1763 `m.room.retention`, 2026-07):** In Room Settings → General → **Message Retention**, an admin picks Off / 1 Day / 1 Week / 1 Month (non-admins see the buttons disabled). After setting e.g. 1 Day, messages older than a day **vanish from the timeline** for everyone in Lotus (toggle Settings → General → **Show Hidden Events** to reveal them again). Setting back to **Off** restores them. Separately, each user can enable Settings → General → **Enforce Message Retention** (default OFF) → their OWN expired messages then get **permanently redacted** within ~30 s (verify: OTHER people's messages are NEVER redacted by this; only your own). Note true server-side purge also needs Synapse `retention:` configured.
|
||||
|
||||
**Mark as Unread + Low Priority (MSC2867 / m.lowpriority, 2026-07):** Right-click a room in the sidebar → **Mark as Unread** puts a dot on the row (bold name) even with no new messages; opening/reading the room clears it, and it syncs to another device. **Mark as Read** on a marked room clears it too. Right-click → **Add to Low Priority** moves the room into a collapsed "Low Priority" category at the bottom of the room list (and removes it from Favorites if it was there, and vice-versa); **Remove from Low Priority** returns it to Rooms.
|
||||
|
||||
**Windows rich toast (D6, 2026-07 — desktop/Windows build only):** get a message notification while the desktop app is backgrounded → the toast is attributed to **Lotus Chat** (not "PowerShell"/generic) and shows an inline **reply box + Send**; typing a reply + Send **posts it to that room**; clicking the toast body **opens the room**. Previously these silently fell back to a plain toast (no reply/click). If it still falls back, check that a `Lotus Chat.lnk` exists in the Start-Menu Programs folder.
|
||||
|
||||
**Invite QR is now generated LOCALLY (2026-07):** Room settings → Share Room → the QR code renders (a black-on-white SVG in a white box) with **no network request** to `api.qrserver.com` (check DevTools Network — there should be no external QR fetch, and it should work offline / behind strict CSP). **Scan it** with a phone camera / Matrix app → it opens the correct `matrix.to` room-invite link. (`api.qrserver.com` was removed from the prod CSP img-src, so a regression would make the QR blank rather than silently phone home.)
|
||||
|
||||
**Unread dot on federated rooms + avatar-decoration console storm (2026-07):**
|
||||
|
||||
- **Read receipts (regression guard — highest priority):** open several rooms and open the Home/Direct tabs (which mark all orphan rooms read on mount) → rooms **stay read**, unread dots clear and don't come back. (A prior attempt sent a receipt for the thread _root_ when a thread's replies weren't loaded, which the SDK treats as a main receipt at an old event and re-unread every room on every mark-read. Fixed + locked by `notifications.test.ts`.)
|
||||
- **Thread dot:** a room with an unread reply in a thread whose replies are loaded → its dot clears on read; for a thread not yet loaded, the dot clears once you open/load the thread. (mark-as-read now sends a threaded receipt only for a genuine loaded reply, never the root.)
|
||||
- With DevTools console open on federated rooms, the `io.lotus.avatar_decoration` `403`/`502` (and federated media) errors should **not** repeat on every scroll/mount — each failing user is now requested at most ~twice per session, so the storm (and its homeserver load) is gone.
|
||||
|
||||
**Custom Window Chrome (Beta) fix (2026-07):** on the desktop build, Settings → General → toggle **Custom Window Chrome** — it should reload and come up with the Lotus title bar and a normal, stable feed (no screen-expand / auto-scroll-into-the-past). Toggle back off → reloads to the native frame.
|
||||
|
||||
_Ported from the retired `LOTUS_BUGS.md` (2026-07). Compact index of shipped-but-not-live-tested items; the detailed steps are in the lettered sections above._
|
||||
|
||||
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 |
|
||||
| P3-8 | Thread Panel (side drawer, chips, threaded receipts, thread composer) | `features/room/thread/*`, `RoomTimeline/RoomInput` | 6-step checklist in LOTUS_TODO §P3-8 |
|
||||
| P4-4 | KaTeX math (`$…$`, `$$…$$`, data-mx-maths; lazy chunk) | `utils/mathParse.ts`, `components/math/` | send `$x^2$`, `$$\int f$$`, `$5 and $10` (stays text), math inside code block (stays text) |
|
||||
| P4-8 | Encrypted-search cache (opt-in toggle, clear button, logout wipe) | `utils/searchCache.ts`, message-search | enable in search panel → search → reload → coverage persists; logout wipes |
|
||||
| N97a | Session blob migration + cross-tab logout sync | `state/sessions.ts`, `useSessionSync` | login on old build → new build migrates; logout in tab A → tab B drops to auth |
|
||||
| P4-1 | Slack-style thread notifications (participating default, All/Mentions/Mute, badge math) | `utils/threadNotifications.ts`, `ClientNonUIFeatures`, `roomToUnread` | 6-step checklist in LOTUS_TODO §P4-1 |
|
||||
| AW-1 | Scheduled-message cancel no longer ghost-sends (error row on failure) | `ScheduledMessagesTray.tsx` | schedule → cancel with network cut → item stays + error; retry works |
|
||||
| AW-2 | Emoji lazy-load (search/autocomplete/recents fill in; board opens fast) | `plugins/emoji.ts` + consumers | first emoji-board open of a session: grid+search populate; reactions still label |
|
||||
| AW-3 | SW precache (repeat-visit near-instant; deploys still picked up immediately) | `sw.ts`, `vite.config.js` | load app twice (2nd = cached assets); deploy → reload picks new version |
|
||||
| AW-4 | Desktop CSP tighten + Escape/panel fixes + thread Jump to Latest | `tauri.conf.json`, Room/ThreadPanel | desktop: boots, avatars/media load, VT323 font renders, location maps embed, calls connect, deep links work |
|
||||
| P3-4 | Accessibility compliance pass (collapsed-msg SR sender, form/overlay labels, typing announce, focus-return, `?` help, jsx-a11y CI gate) | `message/*`, `RoomViewTyping`, `features/shortcuts/*`, `eslint.config.mjs` | LOTUS_TESTING §P — axe-core + VoiceOver/NVDA on the golden path |
|
||||
| P6-1 | Desktop Linux parity (no-sleep in calls, launcher badge), autostart toggle, tray Do-Not-Disturb | `native/power.rs`, `lib.rs`, `useTauriDnd`, `General.tsx` | Linux desktop: no display sleep during a call; tray DND silences notifications; launch-on-login persists; Unity badge (Ubuntu); DND toggle polarity |
|
||||
| P6-2 | EC deafen/screenshare-audio-mute via `io.lotus.set_deafen` (retires the `<audio>.muted` iframe hack) | fork `lotusDeafen.ts`, cinny `CallControl.ts` | AFTER publish+pin-bump: deafen silences remote audio + survives a reconnect / new screenshare / late joiner (the cases the DOM hack failed); screenshare-audio-mute toggles independently |
|
||||
| P6-3 | Forward-to-multiple-rooms (multi-select + partial-failure summary) + live bookmark previews (edits/redactions, snapshot fallback) | `ForwardMessageDialog.tsx`+`forwardContent.ts`, `BookmarksPanel.tsx` | forward one msg to 3 rooms (incl. 1 you cannot post to = partial summary); bookmark then edit shows edited; redact shows deleted; leave room shows snapshot |
|
||||
| P6-4 | HSTS + Permissions-Policy on prod nginx (+ contrib examples) | `matrix/cinny/nginx.conf`, `contrib/nginx`, `contrib/caddy` | after `nginx -s reload`: `curl -sI https://chat.lotusguild.org` shows HSTS + Permissions-Policy; a call (cam/mic/screenshare) + location share still work |
|
||||
|
||||
**Verified working in live testing (2026-06):** A2, B1–B4, 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.
|
||||
|
||||
---
|
||||
|
||||
### Green CI but the fix isn't live?
|
||||
|
||||
-418
@@ -1,418 +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)
|
||||
|
||||
> Completed features are documented in [LOTUS_FEATURES.md](./LOTUS_FEATURES.md). Manual test steps live in [LOTUS_TESTING.md](./LOTUS_TESTING.md). This file is **open work only** — resolved audit findings and shipped-feature write-ups were removed 2026-07 (full history in git).
|
||||
|
||||
Status legend: `[ ]` pending · `[~]` in progress / shipped-awaiting-QA · `[x]` done · `[BLOCKED]` server/upstream-gated · `[DEFERRED]`/`[DROPPED]`/`[WON'T FIX]` decided.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 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. Canonical tokens: `--lt-accent-orange`, `--lt-accent-cyan`, `--lt-accent-green`, `--lt-glow-*`, `--lt-box-glow-*`, `--lt-border-color`, `--lt-font-mono`. Syntax-highlight token classes: `.tok-kw .tok-str .tok-num .tok-cmt .tok-fn`.
|
||||
> Reference patterns: `/root/code/tinker_tickets/` (markdown.js, base.js, ticket.css). Applies to every task without exception.
|
||||
> New components must respect both TDS dark (`LotusTerminalTheme`) and TDS light (`LotusTerminalLightTheme`); non-TDS theme work uses vanilla-extract (match `src/lotus-terminal.css.ts`).
|
||||
|
||||
## 🧩 NATIVE-CINNY LAW — EVERY FEATURE MUST FEEL LIKE STOCK CINNY
|
||||
|
||||
> **Every feature must feel native to upstream Cinny — indistinguishable from what the Cinny team would ship.** Reference: <https://github.com/cinnyapp/cinny>.
|
||||
>
|
||||
> - **Use the `folds` design system, not bespoke UI** (`Button`, `Chip`, `IconButton`, `Menu`, `MenuItem`, `Dialog`, `Modal`, `Input`, `Switch`, `Badge`, `SettingTile`, `SequenceCard`, …) and folds tokens (`color.*`, `config.space.*`, `config.radii.*`). **Use folds `Icon`/`Icons`, never literal emoji, in UI chrome.** No hardcoded hex/`rgba()`, no invented CSS variables.
|
||||
> - **Match Cinny's existing patterns** — find the closest existing component/flow and mirror it before adding UI.
|
||||
> - **The ONE exception:** explicit **TDS** features, which follow the TDS Design Law above (opt-in, only in Lotus Terminal mode).
|
||||
|
||||
---
|
||||
|
||||
## ✅ Audit (2026-07) — closed out
|
||||
|
||||
A three-wave feature bug-hunt (~15 parallel agents, each batch independently reviewed) plus a low-tail cleanup. All confirmed 🔴/🟠 and the clean 🟡 tail are **fixed, reviewed, and gate-green**; details in git history + LOTUS_FEATURES. Only the minor items below remain open.
|
||||
|
||||
**Still open (low tail — all 🟡 minor):**
|
||||
|
||||
- ✅ **Low-tail batch FIXED** (`a267e9e9`, 2-agent-reviewed, gate-green): **T5** (`participated` now also scans the local thread timeline, not just the server bundle → no under-notify), **T6** (room "Mentions & Keywords" honored for Default thread replies via a new `roomMentionsOnly` gate → no over-notify; +4 tests), **T7** (thread-mode account-data writes serialized with content carried forward → no lost update), **C-L2** (a real incoming ring cancels a lingering Settings preview), **C-L3** (ringtone AudioContext primed on first page gesture → first ring after cold load not silent), **C-L5** (`useCallSpeakers` depends on a stable boolean → no observer churn on membership change), **F5** (OIDC refresher forwards the refreshed token `expiry` as `expiresInMs` → `expiresAt` no longer stale across reloads). **Verified already-handled, no change:** **N6** (`useMemberAvatar` already subscribes via `useRoomMemberChange`), **H10** (`RoomProfile` already has `maxLength={255}` + surfaces the submit error).
|
||||
- **Calls host (still open):** C-M1 deafen DOM-fallback leaks late-added `<audio>` tracks; C-M2 `.click()`-by-testid toggles no-op if EC renames — **both retire via EC-fork P6-2**. C-L1 AFK mic not released if EC elides the echo; C-L7 all-muted DOM miscount if EC label format differs; C-L8 PiP sw/nw resize anchor jitter at min size. **All four are EC-DOM/echo-behavior or visual-jitter items — need a real call + the EC iframe to verify; deferred.**
|
||||
- **Native/desktop:** D7 Unity badge `application://cinny.desktop` id may not match the installed `.desktop` basename — **runtime-verify** on the `.deb`/AppImage.
|
||||
- **EC fork (EC1–EC6 fixed on `element-call:lotus`, needs a republish):** re-apply `setTimeout` cleanup, remote-gated subscription → `allConnections$`, per-call decoration state leak, re-subscribe-every-render, focus-clear on missing `userId`. Rides with **P6-2 phase 2**.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Shipped — Awaiting Live Verification
|
||||
|
||||
Built and gate-green; verify per [LOTUS_TESTING.md](./LOTUS_TESTING.md), then graduate to LOTUS_FEATURES.md. Includes the **desktop/native Tier A/B stack** (P5-35/36/41/42/43/44/46/47/48/49/55/56/57, P6-1 Linux parity) — all **CI-compile-verified, runtime-verify on Windows/Linux** — plus:
|
||||
|
||||
| Area | Test guide |
|
||||
| :-------------------------------------------------------------------------- | :-------------------- |
|
||||
| Full-Screen Camera Broadcasts (per-participant focus) | A5 / G2 |
|
||||
| Advanced search filters + virtualized infinite scroll | K2 / M1 / M2 / M4 |
|
||||
| Custom Accent Color Picker (non-TDS) · 5 Color Theme Presets | M3 / M5 |
|
||||
| Intersection lazy media loading · context-aware thumbnails | H1 / H2 |
|
||||
| Thread Panel (side drawer) + per-thread notification modes (P4-1) | (thread QA) |
|
||||
| Encrypted message search indexing/caching (opt-in, default OFF) | search backlog |
|
||||
| Remind Me Later · Mobile Bookmarks access | K1 / E5 |
|
||||
| In-Call Soundboard (P5-15) · Quality Controls (P5-31) · Permissions (P5-31) | D2-7 / D2-8 / D2-9 |
|
||||
| Desktop proactive update notifications (P5-40) | J1 |
|
||||
| OIDC/SSO login (P4-6, needs an MSC3861 server — pick mozilla.org on login) | OIDC |
|
||||
| Windows native WinRT toast quick-reply / click-to-open (D6, AUMID) | rich-toast (§backlog) |
|
||||
| Inline media embeds (16 providers: video/audio/post + click-to-play facade) | Q1 / Q2 / Q3 / Q4 |
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Open — Actionable
|
||||
|
||||
### ✅ Discovery pass (2026-07) — DP1–DP18 DONE
|
||||
|
||||
Agent-surveyed + TPVR-verified correctness / a11y / tech-debt fixes (DP1–DP18) are implemented, review-fixed, and gate-green (tsc + eslint + 737 tests + build). Verify per [LOTUS_TESTING.md](./LOTUS_TESTING.md) §R, then remove this note. Full detail in git history — commits `8eb961b6` `db864326` `6cf18c3b` `165714e1` `8c0e2b42` `e545706c` `b1ee3ada` `4fc3f7a3` `101e4116` `4fa4327a` `c2598d21`.
|
||||
|
||||
### ✅ Discovery pass 2 (2026-07) — perf / security / correctness — DONE
|
||||
|
||||
Agent-surveyed findings, each **verified against the code before fixing**, then implemented and gate-green (tsc + eslint + prettier + 857 tests + build), with **two review agents on every staged diff before commit**. Verify per [LOTUS_TESTING.md](./LOTUS_TESTING.md), then remove this note.
|
||||
|
||||
- [x] **PERF-1 — presence: 3 client listeners PER avatar → one shared presence store** (3 listeners total). `8a154051`.
|
||||
- [x] **PERF-2 — `#`-mention autocomplete mutated + re-sorted the shared `allRoomsAtom` every keystroke** → copy + `useMemo` (also fixed a real shared-array mutation hitting ~27 consumers). `4708a179`.
|
||||
- [x] **PERF-3 — read-receipt rows: ~6 global `Members` listeners per row → one shared member-change store** (`useRoomMemberChange`). `1b8f5545`.
|
||||
- [x] **PERF-4 — message-search room filter re-sorted every render** → `useMemo`. `4708a179`.
|
||||
- [x] **PERF-5 — DM-preview `Decrypted` listener ran for every nav item** → gated on `direct`. `4708a179`.
|
||||
- [x] **SEC-1 / SEC-2 — scheduled-message plaintext + recent searches survived logout** → `clearPlaintextCaches()` on both logout paths, extended to the whole `recent_*` family + nav-paths. `726cefb5`.
|
||||
- [x] **SEC-3 — `window.open(_blank)` without `noopener`** → `noopener,noreferrer` at 5 sites (SSOStage excluded — needs the handle). `3e1106b2`.
|
||||
- [x] **SEC-4 — `/acl` self-lockout footgun** → shared `serverAcl.ts` validation, no-brick allow default, fail-closed self-ban guard. `3e1106b2`.
|
||||
- [x] **COR-1 — space-child UNLINK over-deleted** → targeted `UNLINK` reducer action. `fd3b8b42`.
|
||||
- [x] **COR-2 — `useCallJoined` stuck true on 2nd-call embed swap** → re-seed on `[embed]`. `1f80d1d1`.
|
||||
- [x] **COR-3 — incoming-call lifetime guard only corrected future clock-skew** → `Math.abs(...)` (±20s). `ab01d27a`.
|
||||
- [x] **COR-4 — shared notify-dedupe slot double-notified** → key by `roomId|threadId`. `1f80d1d1`.
|
||||
- [x] **COR-5 — upload cancel ignored during retry back-off** → `AbortSignal` threaded into the retry loop. `ab01d27a`.
|
||||
- [x] **COR-6 — `CallControl.forceState` dropped `screenshareAudioMuted`** → passes it. `ab01d27a`.
|
||||
|
||||
**Deferred / decided (not built):**
|
||||
|
||||
- [DEFERRED] **PERF-6 — avatar-decoration `/profile` fetch** — well-guarded (module cache + in-flight dedupe + backoff); a network/HS-load note only. Batch/skip only if it proves costly.
|
||||
- [DEFERRED] **SEC-5 — embeds' `allow-popups-to-escape-sandbox`** — informational; main-app hijack already prevented (no `allow-top-navigation`), and popups are arguably needed for "open in provider." Revisit with per-provider verification if dropped.
|
||||
- **KE-1 preventive (`navigator.storage.persist()`)** is **already implemented** (`initClient` → `requestPersistentStorage()` + `src/index.tsx` boot). The rest of the KE cluster stays under **Encryption / E2EE** below (needs live capture).
|
||||
|
||||
### 🔍 Feature bug hunt (2026-07, 5-agent, LOTUS_FEATURES surface) — open findings
|
||||
|
||||
Per-slice bug hunt (5 agents: theming · calls · messaging · threads/presence/UX · rooms/mod/notif/infra/desktop), each **verified against current code** (already-fixed items not re-flagged; the heavily-audited hot paths came back clean). Residual findings below. `[live]` / `[desktop]` = needs a real call / the desktop app to confirm.
|
||||
|
||||
**Embeds / URL previews**
|
||||
|
||||
- [x] **[Med] Desktop (Tauri) CSP `frame-src` was missing `store.steampowered.com`, `www.mixcloud.com`, `widget.deezer.com`** → the Steam widget (shipped) + new Mixcloud/Deezer embeds were silently blocked (blank iframe) **in the desktop app**. **FIXED** (`cinny-desktop` `daba59b`): all three added to `frame-src` (no `connect-src` — these don't do a client oEmbed fetch). Web was always fine (`frame-src 'self' https:`). Needs desktop-app QA to confirm the widgets render.
|
||||
- [x] **[Low]** `searchCache.ts` encrypted-search index has no size/count cap — unbounded on-disk growth (mitigated by the manual "Clear cached index" + logout wipe). **FIXED** (`fff811cb`): per-room cap of 5000 rows, oldest-by-ts evicted on write via a self-chaining IDB cursor + pure unit-tested `evictCount`. IDB-spec correctness (cursor delete/continue, tx liveness, range bracketing) confirmed by 2 review agents since CI can't run IndexedDB.
|
||||
- [x] **[Low]** `MsgTypeRenderers.tsx` `MLocation` OSM permalink uses raw `geo:` lat/lon substrings, not the validated floats — harmless (URL context, malformed input only). **FIXED** (`8a461610`): permalink uses the `parseFloat`+`isFinite` validated `lat`/`lon` (as the map iframe already did).
|
||||
|
||||
**Voice / video calls**
|
||||
|
||||
- [x] **[Med]** `DenoiseTester.play()` (Settings → Calls A/B model test) leaks the denoise model node — calls `ctx.close()` but never `denoise.dispose()` (inconsistent with `stopLive`, which disposes) → leaks the DeepFilterNet/DTLN worker/WASM per press. **FIXED** (`c9d9d914`): `stopPlayback` now mirrors `stopLive` (dispose model + gate), and a generation token also closes the rapid-click / stop-during-load / unmount-during-load leak windows (3 review passes, all 6 interleavings traced).
|
||||
- [x] **[Med] [live]** PiP auto-spotlight never released on return to the call room — the release branch sits inside the `if (!pipMode) return` guard, so screenshare→PiP→back leaves spotlight forced on and `pipAutoSpotlightRef` stuck `true`. `CallEmbedProvider.tsx:733-744`. **FIXED** (`08e19100`, code-level; still wants live QA): effect guards only on `!callEmbed`, releases whenever `pipMode && pipScreenshare` is false; + ref-reset on embed teardown + deps comment (2-agent reviewed).
|
||||
- [x] **[Low]** DenoiseTester async paths (`getUserMedia`) have no mounted-guard → ctx/stream leak + setState-after-unmount if Settings closes during the mic prompt. **FIXED** (`c9d9d914`): a `mountedRef` guards `startLive`/`startRecord` after the `getUserMedia` await (and `play()` after its model load); the ref is set on mount, not only cleared on unmount, so it survives a StrictMode/Activity remount.
|
||||
- [x] **[Low]** Soundboard 30s safety timeout never cleared on natural clip end (`CallSoundboard.tsx:115`); `PrescreenControls` `PermissionStatus.onchange` not removed on unmount (`PrescreenControls.tsx:22-28`). **FIXED** (`56561627`): per-play timer token cleared on end/unmount (identity-guarded so a stale clip can't disarm a newer one); permission `onchange` detached + `cancelled`-guarded setState.
|
||||
- [ ] **[Low] [live]** Call-to-call switch disposes the embed without an explicit `HangupCall` → possible transient ghost RTC membership until EC's unload-leave fires.
|
||||
|
||||
**Theming / visuals**
|
||||
|
||||
- [x] **[Med]** `invalidateDecorationCache` clears the module cache but has no pub/sub → changing **your own** avatar decoration doesn't update live in already-mounted avatars (timeline/members) until remount. Add a listener set / bump counter. `useAvatarDecoration.ts:67`. **FIXED** (`29ff1654`): per-user listener set notified on invalidation (+ clears the give-up counter); concurrent re-fetches de-dupe via the existing `pending` map.
|
||||
- [x] **[Med/Low]** Decoration picker grid thumbnails use the raw `DECORATION_CDN` constant instead of `decorationUrl()`, ignoring the `VITE_DECORATION_CDN` override → broken thumbnails if decorations are repointed. `ProfileDecoration.tsx:51`. **FIXED** (`29ff1654`): grid uses `decorationUrl(slug)`.
|
||||
- [x] **[Low]** Seasonal "Auto" is computed once at mount (no ticker, unlike NightLight) → won't flip across a holiday-window boundary in a long-lived session. `SeasonalEffect.tsx:100`. **FIXED** (`d416c62b`): hourly re-eval ticker (auto only) + refresh on entering auto; decision extracted to pure `resolveSeasonTheme` + tested.
|
||||
- [x] **[Low]** Selecting seasonal "Auto" while a chat background is set is a silent no-op (asymmetric mutual exclusion — SeasonalEffect early-returns when `chatBackground !== 'none'`). `General.tsx:550`. **FIXED** (`d416c62b`): any active seasonal mode (incl. auto) now clears the chat background; only "off" leaves it (symmetric with the bg picker).
|
||||
- [x] **[Low]** Decoration settings fetch the `/{field}` sub-resource → console 404 for users with no decoration set. `ProfileDecoration.tsx:79`. **FIXED** (`29ff1654`): reads the full `/profile/{userId}` (matching `useAvatarDecoration`); PUT/save path unchanged.
|
||||
|
||||
**Threads / presence / UX**
|
||||
|
||||
- [x] **[Med]** `PresenceBadge` renders DND (`unavailable` + `status_msg:'dnd'`) as a **yellow "Idle"** badge + label, while `PresenceRingAvatar` correctly shows **red** — inconsistent. Give the badge the same `status === 'dnd' → Critical` + "Do Not Disturb" branch. `Presence.tsx:17-59`. **FIXED** (`29ff1654`): badge now matches the ring + settings picker (Critical / "Do Not Disturb", `'dnd'` sentinel line suppressed).
|
||||
- [x] **[Med]** Collapsible-message threshold is hardcoded (`COLLAPSE_MAX_HEIGHT = 320`), but the docs claim it's "configurable in Settings → Appearance (default 20 lines)" — unimplemented. Add the setting + control, or fix the doc. `MsgTypeRenderers.tsx:38`. **FIXED** (doc): LOTUS_FEATURES now describes the fixed 320px (≈20-line) threshold; the full 320px is sensible and a per-user setting wasn't worth the surface — reconciled the doc rather than build a marginal setting.
|
||||
- [x] **[Med/Low]** In-app toast container has no visible cap / scroll — a burst of messages across rooms while focused stacks toasts unbounded and can cover the viewport. Cap visible N or `overflow-y:auto` + max-height. `LotusToastContainer.tsx:223-247`. **FIXED** (`1963222d`): queue capped at 5 in the atom writer (drops oldest non-sticky, never the newest or a sticky action toast) + container maxHeight/overflow + scroll-to-newest; +4 tests. (3 review passes — the 2nd caught a newest-dropped edge when the cap is full of stickies.)
|
||||
- [x] **[Low]** "Unread First" room sort leaves the (larger) read portion unordered — no activity fallback for the equal-unread case. `Home.tsx:213-222`. **FIXED** (`1963222d`): `factoryRoomIdByUnread` breaks ties by recent activity; relocated to `utils/sort.ts` (pure) + unit-tested.
|
||||
- [x] **[Low]** Tab title "(N)" counts mentions, not unread messages (doc says unread) — reconcile doc vs. code. `ClientNonUIFeatures.tsx:120-123`. **FIXED** (doc): the mention-count + unread-dot behavior is intentional (mirrors the favicon); LOTUS_FEATURES now describes it accurately (N = highlights, `·` = other unread).
|
||||
|
||||
**Rooms / moderation / notifications / infra / desktop**
|
||||
|
||||
- [ ] **[Med] [desktop]** `useTauriFocusAssist` never queries the initial OS Focus-Assist state on mount (unlike `useTauriDnd`, which rehydrates via `get_tray_dnd`) → if Focus Assist is already ON at launch, notifications/sounds leak through until the OS state next flips. Add a `get_focus_assist` mount query (confirm whether the native poll emits an initial reading). `useTauriFocusAssist.ts:18-24`.
|
||||
- [x] **[Low]** Push-rule enable toggle holds stale local `useState` after an external rule change (toggled on another device) — sync from the `pushRule.enabled` prop. `PushRuleEditor.tsx:55-79`. **FIXED** (`2c0cd0d2`): `useEffect` resyncs on `pushRule.enabled` change (prop flows from live `useAccountData(m.push_rules)`; no optimistic conflict).
|
||||
- [x] **[Low]** Server-support `.well-known/matrix/support` is fetched from `mx.getHomeserverUrl()` (client-API host) instead of the MXID **server-name** host → silently missing on delegated/split-domain servers. `About.tsx:45-47`. **FIXED** (`2c0cd0d2`): fetched from `https://{mx.getDomain()}` (MSC1929-correct); identical for non-delegated, graceful catch otherwise.
|
||||
- [x] **[Low]** Cleared/partial quiet-hours `time` input (`''` → window inactive) silently disables the window while the toggle still reads "on" — no feedback. `SystemNotification.tsx:364-382`. **FIXED** (`5175c095`): inline Critical hint when the toggle is on but a time field is empty.
|
||||
- [~] **[Low] [desktop]** Native quick-reply swallows send errors (`.catch(() => undefined)`); the `show_rich_toast` trigger has no verified web-side caller. `useTauriToastActions.ts:35-38`. **ROOT CAUSE FOUND + web fix shipped** (`0ddf86c6`): `show_rich_toast` was dead because `showOsNotification` preferred the service worker (WebView2 has one), shadowing the injected `window.Notification` shim. Now skips the SW path under Tauri → notifications route to the rich toast, whose click navigates to the message.
|
||||
|
||||
### 🖥️ Desktop notification rich-toast — follow-ups (activated by `0ddf86c6`, need a Windows build)
|
||||
|
||||
The web-side nav fix (`0ddf86c6`) makes the native rich-toast path live for the first time. It fixes click→navigate, but exposes latent behaviors in the **cinny-desktop Rust** that need a Windows build to fix + verify:
|
||||
|
||||
- [ ] **[Med] [desktop]** **Tag-coalescing lost.** The web SW notification used `tag` to _replace_ prior notifications for the same room; `show_rich_toast` (`cinny-desktop/src-tauri/src/native/toast.rs`) ignores `tag` and shows a new WinRT toast every time → rapid same-room messages stack instead of collapsing. Fix: dedupe/replace by room in the toast store (`toast.rs:226-230`).
|
||||
- [ ] **[Med] [desktop]** **Thread / invite quick-reply misroutes.** The reply target is the coalescing `tag` — `${roomId}:${threadId}` for thread replies, `'lotus-invites'` for invites (`ClientNonUIFeatures.tsx:471,192`) — not a real room id, so `mx.sendMessage(tag, …)` fails silently (`useTauriToastActions.ts:37`). Body-click navigation is correct (uses `path`). Fix: pass the real `roomId` separately (e.g. `data.roomId`) and have the shim (`lib.rs` `NOTIFICATION_BRIDGE`) + `toast.rs` use it for the reply target; keep `tag` for coalescing. Invite toasts should also drop the reply box (nothing to reply to).
|
||||
- [ ] **[desktop QA] Windows notification checklist** (verify `0ddf86c6` + the above): (1) confirm the pre-fix symptom was focus-without-navigate; (2) message toast → click navigates to the message, quick-reply sends to the room; (3) thread toast → navigates, reply currently misroutes (until fixed above); (4) invite toast → navigates to invites; (5) rapid same-room messages → stacking until coalescing restored; (6) AUMID-missing/dev build → plain-notification fallback still shows; (7) web PWA unaffected.
|
||||
- [x] **[Low]** Export-history date-range early-break can over-paginate + mislabel "truncated" in E2EE rooms (`oldestRawTs` only advances on decrypted `m.room.message`, so undecryptable old events never move it). `ExportRoomHistory.tsx:104,136`. **FIXED** (`3ff8fb8e`): boundary now advances on every event (getTs is envelope metadata), above the type/decryption filters; guarded `ts > 0` so a bogus 0-ts can't cause the opposite (silent under-pagination). 2-agent reviewed.
|
||||
- [x] **[Info/doc]** `PolicyListViewer` is a manual room-ID/alias viewer with **no** subscribe/unsubscribe controls and no subscribed-lists listing — `LOTUS_FEATURES.md:1287` describes both. Docs oversell; not a runtime bug. **FIXED** (`8a461610`, doc): LOTUS_FEATURES corrected to describe the read-only room-ID/alias viewer (no subscribe controls).
|
||||
|
||||
### ✅ Composer autocomplete-insert crash (reported 2026-07) — FIXED (`477df4ae`)
|
||||
|
||||
Picking an autocomplete item (mention/emoji/command) occasionally tripped the composer error boundary ("encountered an error" → forced refresh) even though the element inserted. Root-caused (3 agents, incl. a headless slate simulation) to `moveCursor` deferring its cursor work to `setTimeout`, leaving the caret on the just-inserted inline-void's zero-width edge; slate-react's commit-phase `setBaseAndExtent(voidEdge, 1)` then threw `IndexSizeError` mid-render → boundary. **Fix:** do `Transforms.move` (escape the void) + `insertText(' ')` synchronously in the same commit as the insert, so the caret is a resolvable text point when the selection sync runs. Plus a recoverable boundary ("Reload composer" + `onReset` deselect) so any residual composer crash no longer needs a page refresh. (A first "sync insertText without move" attempt was caught in review — the void guard drops the space + traps the caret; `move` is required.)
|
||||
|
||||
### ✅ Unread/read-receipt flakiness (reported 2026-07) — FIXED (pending prod QA)
|
||||
|
||||
Room unread dots were inconsistent: reading a message sometimes cleared the dot, sometimes left it stuck, sometimes it resurrected. Root cause (confirmed by tracing + diffing upstream cinny `dev`): **our own "N4" change.** `handleReceipt` recomputed via `getUnreadInfo`, which reads `room.getUnreadNotificationCount()` — server-computed and **stale on the synchronous synthetic receipt echo** (SDK only zeroes it immediately when the last event is your own message) → it PUT the stale non-zero count back → stuck/resurrecting. Compounded by `hasUnread = !!unread` lighting the dot on any present map entry, incl. phantom `{0,0}` PUTs from our `UnreadNotifications` listener. Plus a Mark-as-Unread (MSC2867) flag that never cleared on opening an already-read room (no receipt → no auto-clear).
|
||||
|
||||
**Fix:** `roomToUnread.ts` — `handleReceipt` reverts to upstream's optimistic `DELETE` on own receipt; reducer collapses `{0,0}` PUT → DELETE. `notifications.ts markAsRead` clears the marked-unread flag directly. `markedUnread.ts onReceipt` gated to main/unthreaded receipts (`myMainReceiptPresent`). Unit tests added; 700/700 pass, typecheck + build clean. Deploy + manual QA (read → dot clears & stays; thread read; mark-unread → open → clears; reconnect no resurrect).
|
||||
|
||||
### 🧨 Encryption / E2EE — ⚠️ EXTREME COMPLEXITY · 🧠 PLANNING SESSION REQUIRED
|
||||
|
||||
Observed live in prod 2026-06-30 during a 2-person **Element Call** (E2EE). These span client rust-crypto (`matrix-js-sdk@41.7.0`) ↔ Synapse ↔ EC MatrixRTC E2EE and are **interrelated** — do NOT spot-fix. **Capture first:** run **Settings → Developer Tools → Crypto Diagnostics** during the next affected call + a synapse-side trace before any fix. (Full runbook was in `LOTUS_E2EE_INVESTIGATION.md`, now in git history.) None are caused by the EC fork work.
|
||||
|
||||
- **KE-1 — OTK upload conflict storm (CRITICAL, root-cause candidate).** `POST /keys/upload` returns `400 M_UNKNOWN: One time key … already exists` continuously — the rust-crypto store and Synapse have **diverged OTK state** (upstream `matrix-rust-sdk#5200`, OPEN: on the 400 the SDK never marks the request sent → re-uploads forever; **not** fixed in 41.7.0). Leading web trigger: cinny never calls **`navigator.storage.persist()`**, so the IndexedDB crypto store is evictable while the `localStorage` session survives → device resurrects with a blank store. **Buildable preventive fix (no call needed):** request persistent storage on login (+ optional multi-tab guard + a 400-loop→recovery prompt). Healing an already-diverged device still needs a clean logout+login.
|
||||
- **KE-2 — EC media keys not arriving/decrypting → audio/video cut out (CRITICAL).** `MissingKey … for participant`, unexpected encrypted to-device `io.element.call.encryption_keys`. Almost certainly downstream of KE-1 (broken Olm sessions). This is the "friend's audio cuts out" symptom.
|
||||
- **KE-3 — Timeline decrypt error: missing `algorithm` field (HIGH).** rust-crypto can't parse a malformed/legacy encrypted event — capture the offending event id + raw content.
|
||||
- **KE-4 — MatrixRTC delayed-event / membership timeouts (MEDIUM-HIGH).** `Restart delayed event timed out`, repeated `msc4157.update_delayed_event` — may be partly HS responsiveness; correlate with synapse latency. Same planning session (shares the call-reliability surface).
|
||||
|
||||
### Security & Privacy
|
||||
|
||||
- **N97 — Access token + device id in plaintext `localStorage`** (`state/sessions.ts`), XSS-exposed. Architectural — needs a token-protection / session-storage redesign.
|
||||
- **Persisted PII without encryption:** user status message + expiry (`Profile.tsx`), unsent composer drafts (`RoomInput.tsx`). Leak risk on shared devices.
|
||||
|
||||
### PWA / Offline / Web Push
|
||||
|
||||
- **N107 — Web Push is non-functional:** `src/sw.ts` has no `push` handler. Needs a `push` listener + Matrix push-gateway integration. **The one substantive remaining feature** (session/crypto groundwork it waited on has landed).
|
||||
- **No app-asset caching strategy** in `src/sw.ts` — no offline capability.
|
||||
|
||||
### Dependencies / Build / Hygiene
|
||||
|
||||
- Build-time: `lotusDenoise` does heavy sequential `fs` in `closeBundle`; `viteStaticCopy` has redundant renames — could be streamlined.
|
||||
- `patch-folds.mjs` edits `node_modules` directly (robust today; `patch-package` considered but more brittle to folds restructuring — WON'T-DO unless it breaks).
|
||||
- `types/matrix/` mirrors SDK types instead of importing them — drift risk; spot-fix highest-risk only.
|
||||
- `contrib/nginx`/`contrib/caddy` examples: headers + `try_files` already synced with prod; the prod nginx `add_header` isn't inherited by cache `location` blocks (pre-existing; SPA entry `/` still gets all headers).
|
||||
- `as any` casts across `src/` — gradual typing cleanup. Keep commits scoped (bisect-friendly). Keep README fork-sync version/logo current.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Matrix Protocol Gaps
|
||||
|
||||
Genuine Matrix client-spec / MSC features Lotus does **not** yet implement (audited 2026-07 against the codebase — almost everything else is built: pinning, stickers+picker, room directory, mutual rooms MSC2666, blurhash, key backup/recovery/SSSS, SAS verification, ignore list, invite spam-filter, voice messages, polls, threads, spaces, OIDC, extended profiles, delayed events, authed media). Build each **fully** — spec-correct events, native-Cinny folds UI, tests. Order = clean wins first.
|
||||
|
||||
**Phase A ✅ (2026-07, gate-green 683 tests):**
|
||||
|
||||
- [x] **Mark as Unread — MSC2867 `m.marked_unread`.** Room account data `{ unread: true }` (+ unstable `com.famedly.marked_unread`) via `mx.setRoomAccountData`; clear on read. Context-menu item in `RoomNavItem` + light the existing unread dot; integrate `state/room/roomToUnread.ts`.
|
||||
- [x] **Low Priority rooms — `m.lowpriority` tag.** Mirror the favourite impl (`RoomNavItem.tsx:331-337` `setRoomTag/deleteRoomTag` + the favourites category in `home/Home.tsx`): context-menu toggle + a collapsed "Low Priority" category sorted to the bottom, excluded from normal unread nudging.
|
||||
|
||||
**Phase B ✅ (2026-07, gate-green 688 tests):**
|
||||
|
||||
- [x] **Disappearing Messages — MSC1763 `m.room.retention`.** PL-gated room-settings `SettingTile` to set `{ max_lifetime }`; retention badge; a client-side sweep hides/self-redacts own expired events (pattern like the mute-timer restore in `ClientNonUIFeatures.tsx`). True server deletion also wants Synapse `retention:` (LXC 151).
|
||||
- [x] **QR Device Verification — reciprocate QR.** Add the QR path beside emoji-SAS in `components/DeviceVerification.tsx`: render with `qrcode.react` (already a dep), scan via `BarcodeDetector` (fallback `jsQR`); uses the SDK `VerificationRequest` QR/reciprocate support.
|
||||
|
||||
**Phase C (Room Widgets ✅ 2026-07; Sliding Sync ❌ evaluated — parked):**
|
||||
|
||||
- [x] **Room Widgets — MSC1236 + widget API.** No general widget UI exists (only the PL entry `im.vector.modular.widgets`; the EC call widget is hardcoded). Read `im.vector.modular.widgets`/`m.widget` state, add an Add/Manage panel + sandboxed iframe renderer via `matrix-widget-api` — **extend the existing EC widget plumbing** (`plugins/call/CallEmbed.ts`). Enables Etherpad/notes/dashboards/integrations.
|
||||
- **[PARKED] Sliding Sync — MSC3575 / simplified MSC4186** (evaluated 2026-07, 3 research passes). Server side is GA (`simplified_msc3575`), but the **client** side is not viable for a safe rollout: matrix-js-sdk's `SlidingSync`/`SlidingSyncSdk` are `_internal_`/`@experimental` (Element shipped labs-only, never GA in ~2 yrs, moved to the Rust SDK); **presence isn't delivered over sliding sync** (regresses Lotus presence badges/rings/status); **no upstream Cinny impl** to follow; and Cinny's whole nav (sidebar/spaces/DM/unread) is derived from the **full local room set** (`allRoomsAtom` ← `mx.getRooms()`), so ~14 subsystems (4 core) need re-architecting to a server-windowed list. ~10% confidence a full rollout wouldn't break/regress (missing rooms/messages/unread = worst failure class). **Revisit only if we adopt the Rust SDK or accounts grow large enough that startup latency is a real complaint; an off-by-default experimental spike is possible but not recommended.** Full assessment: git plan history.
|
||||
|
||||
**Room Widgets v1 follow-ups:** capability-approval consent prompt (let widgets request send/read room events); Jitsi/stickerpicker special types; account-data (user/sticker) widgets; per-widget popout / always-on-screen. Requires the prod CSP `frame-src` widening (done in `matrix/cinny/nginx.conf` → **`nginx -s reload`**) or external widgets are blocked.
|
||||
|
||||
**Server-gated / advanced (capture, don't build yet):** QR sign-in for a new device (**MSC4108** rendezvous — needs an HS-side endpoint); dehydrated devices (**MSC3814** — offline key delivery, also helps the E2EE KE cluster); E2EE history key sharing on invite (**MSC3061** `shared_history`, niche); voice broadcast (Element MSC3888, low value — skip).
|
||||
|
||||
### [PARKED] Matrix 2.0 call membership — MSC4354 Sticky Events (investigated 2026-07, 3 agents + live infra check)
|
||||
|
||||
Move MatrixRTC/Element Call call-membership from state events (MSC3401) to **sticky events** — the "Matrix 2.0" path. **Not a flag flip; a coordinated rollout. Parked deliberately.**
|
||||
|
||||
Findings:
|
||||
|
||||
- **Server (Synapse 1.157.1, LXC 151):** `msc4354_enabled` defaults `false`. Enabling is **low-risk, additive, reversible** — schema (`sticky_events` table) already ships unconditionally, no migration/backfill, all runtime paths flag-gated, residual rows self-expire ≤1h. The one historical `/sync` EDU-filter bug (#19787) was fixed in 1.155.0; SQLite guard N/A (we're Postgres).
|
||||
- **The flag alone is a no-op for behavior.** Our EC fork (upstream **v0.20.1** base, `@lotusguild/element-call-embedded`, bundled into Cinny at build → fleet upgrades atomically) gates sticky mode behind BOTH server support AND a per-device **developer-settings** radio (`matrix-rtc-mode`, defaults `Legacy`). Enabling the flag only un-greys that radio; no client changes what it sends until a human toggles it.
|
||||
- **Matrix-layer mixed-mode = safe:** js-sdk (v41.6.0) reads + merges sticky and state membership, so cross-mode participants see each other.
|
||||
- **Open risk before any real rollout:** media layer. Sticky mode drops `livekit_alias` + uses lk-jwt-service `/get_token` (slot `m.call#ROOM`); legacy uses `/sfu/get` (`room=roomId`). Both endpoints are **live** on our lk-jwt-service, but whether they resolve to the **same LiveKit room** is unverified — must confirm with a **two-account cross-mode test call** (one device `Matrix_2_0`, one `Legacy`) before changing the default, else split-at-media.
|
||||
|
||||
To actually adopt (future): (1) enable `msc4354_enabled: true` + restart; (2) two-account media-interop test; (3) if unified, flip EC default mode `Legacy`→`Compatibility`/`Matrix_2_0` in the fork + redeploy; (4) keep legacy fallback during transition. **No user benefit until step 3.**
|
||||
|
||||
### [ ] Matrix 2.0 call membership — MSC4354 sticky events (INVESTIGATED 2026-07, deliberately NOT enabled)
|
||||
|
||||
3-agent investigation after the 1.157.1 upgrade (EC-fork behavior · Synapse/upstream readiness · client-fleet composition). **Conclusion: leave `msc4354_enabled` OFF for now** — enabling it is safe but delivers **zero user-visible benefit on its own**, and introduces a latent footgun.
|
||||
|
||||
**Why it's a no-op alone:** the EC fork's `doesServerSupportUnstableFeature(MSC4354)` probe feeds **exactly one thing** — whether the "Matrix 2.0" radio in **Developer Settings** is greyed out (`DeveloperSettingsTab.tsx:349-353`). The real switch is the per-device `matrixRTCMode` setting (`settings.ts:149-152`), which **defaults to `Legacy`** and never auto-enables. Sticky sending is gated at `LocalMember.ts:862` (`unstableSendStickyEvents: mode === Matrix_2_0`). So flipping the server flag changes nothing any client sends.
|
||||
|
||||
**Verified safe:** Synapse-side is **additive and cleanly reversible** — the `sticky_events` schema ships unconditionally (no migration/backfill on enable), every write/read/serialize/replication path is flag-gated, disabling stops it instantly and residual rows self-expire ≤1h. The one relevant bug (#19787 `/sync` EDU-filter) was fixed in 1.155.0; the SQLite<3.40 guard doesn't apply (we're on PG 17.10). Matrix-layer **mixed-mode visibility is safe**: js-sdk `collectMembersEvents` reads **both** sticky and state membership and merges them, so sticky-mode and legacy-mode participants see each other. Our `lk-jwt-service` already serves **both** JWT endpoints (legacy `/sfu/get` **and** the sticky-mode `/get_token` — both probed live, 400-with-validation-error = present). EC is bundled into cinny's build (`@lotusguild/element-call-embedded`), so the fleet upgrades **atomically** — the "all EC clients ≥ v0.17.0" precondition is structurally guaranteed for our own users.
|
||||
|
||||
**The one unresolved risk (blocks a real rollout, not the flag):** sticky mode drops `livekit_alias` and uses `/get_token` (slot `m.call#ROOM`) while legacy uses `/sfu/get` (`room=roomId`). **Whether both resolve to the same LiveKit room is a property of lk-jwt-service, not the client** — unverified. If they diverge, cross-mode participants appear in each other's member list but are **split at the media layer** (silent, no error). Requires a **two-account test call** (one device on Legacy, one on Matrix 2.0) to confirm before anyone relies on it.
|
||||
|
||||
**If we ever do this:** (1) run the two-account media-interop test; (2) only then consider enabling `msc4354_enabled: true` in `/etc/matrix-synapse/homeserver.yaml` (LXC 151) + restart; (3) treat a default-mode change as a separate coordinated EC rollout. MSC4354 is still **OPEN upstream** (not in FCP, `needs-implementation`), so this stays experimental regardless.
|
||||
|
||||
### Remaining spec/MSC gaps (2026-07 full-surface survey)
|
||||
|
||||
After Phases A–C the client spec is ~complete. What's left, flagged by **what unblocks it**:
|
||||
|
||||
**✅ Buildable NOW (client-only, no server/infra change):**
|
||||
|
||||
- [ ] **Custom room tags / sections** (Gitea **#108**, milestone _Features 2026-Q4_) — user-defined room categories in the sidebar via standard `u.*` room tags (beyond the built-in Favourite / Low-Priority). Mirrors the favourite/low-priority category pattern (`RoomNavItem` context-menu + `Home.tsx` categories). _Medium._ The only substantive client-only feature left.
|
||||
|
||||
**🔧 Needs INFRASTRUCTURE (NOT a Synapse-flag flip — you'd have to stand it up):**
|
||||
|
||||
- **Invite by email / 3PID invite** — we invite by Matrix user-ID only (`mx.invite` is user-ID-only). Email invites need an **identity server** (lotusguild runs none). Build only if an identity server is deployed.
|
||||
- QR sign-in for a new device (**MSC4108**) — needs a **rendezvous** endpoint. Dehydrated devices (**MSC3814**) — needs server support. (Also listed above.)
|
||||
|
||||
**🚫 BLOCKED until a Synapse upgrade enables the flag** — re-run `/_matrix/client/versions` `unstable_features` after each upgrade; client work is ready the moment the flag flips. See the **Blocked Features** section below:
|
||||
|
||||
- Live Location Sharing (**MSC3489** + **MSC3672** — both `false`)
|
||||
- Reaction / relation redaction (**MSC3892** — `false`)
|
||||
- ~~Room preview before joining (MSC3266)~~ — **DONE** (client was always built; unstable `im.nheko.summary` endpoint returns 200 — verified on 1.156)
|
||||
- Thread subscriptions (**MSC4306** — `false`)
|
||||
|
||||
**Niche / low-value (noted, not planned):** E2EE history-key-on-invite (MSC3061), voice broadcast (MSC3888), a native account-deactivation flow (currently delegated to the OIDC provider for OIDC accounts).
|
||||
|
||||
**Already implemented (verified, not gaps):** space reordering (drag — confirmed working in the desktop client), pinning, stickers + picker, room directory, mutual rooms (MSC2666), blurhash, key backup / recovery / SSSS / cross-signing / key export-import, SAS **and** QR verification, ignore list, invite spam-filter, voice messages, polls, threads + per-thread notifs, spaces, OIDC, extended profiles, delayed/scheduled events, authed media, report user/room/message, 3PID contact-info display, disappearing messages, mark-unread, low-priority, room widgets.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Open Feature Backlog
|
||||
|
||||
**Features 2026-Q4 milestone (Gitea):** #103 tracking-param stripping ✅ · #104 settings sync ✅ · #105 desktop keychain + idle lock (research) · #106 search operators (research) · #107 paste-as-code (research) · #108 custom room sections.
|
||||
|
||||
### [ ] Basic in-app audio editor / video→audio extractor (LARGE PROJECT)
|
||||
|
||||
A minimal audio editor for soundboard clips and voice content. Scope: (1) **trim/clip** an audio file to a chosen start/end (waveform scrubber, in/out handles); (2) **upload a video file → strip and discard the video track, keep only the audio** (extract audio, then the source video is dropped — never uploaded/stored); (3) minimal edits only (trim, maybe gain/normalize, fade in/out) — not a full DAW. Likely Web Audio API (`AudioContext.decodeAudioData` → trim `AudioBuffer` → re-encode) + `MediaRecorder`/an encoder for output; video demux via a `<video>`+`MediaElementSource` capture or ffmpeg.wasm (weigh bundle cost). Feeds the soundboard uploader (`utils/soundboardClips.ts`, `SoundboardPackEditor`) and attachments. Design under TDS + native-cinny law. Big build — plan a dedicated session; evaluate ffmpeg.wasm size/CSP (wasm) before committing.
|
||||
|
||||
### [x] P4-4 · Math / LaTeX Rendering — DONE
|
||||
|
||||
Rendering shipped (KaTeX, `$…$`/`$$…$$` + spec `data-mx-maths`, lazy-loaded,
|
||||
`<pre>/<code>`-guarded) — see LOTUS_FEATURES.md. **Outgoing cross-client interop
|
||||
added (2026-07):** the composer now emits spec `data-mx-maths` HTML on send
|
||||
(`editor/output.ts`, reusing `splitMathSegments`), so math a Lotus user types
|
||||
renders on Element and every other client, not just Lotus. Deferred: multi-line
|
||||
block `$$…$$` (spans editor paragraph nodes) still renders on Lotus via the
|
||||
plain-body `$…$` path only.
|
||||
|
||||
### [~] P5-20 · Quick Reply from Browser Notification (partial)
|
||||
|
||||
Done: notifications show the real body, click navigates to the specific event + focuses the tab. **Remaining:** inline reply via Notification Actions API needs the SW `push`+`notificationclick` pipeline (switch `new Notification()` → `serviceWorkerRegistration.showNotification()` so the SW receives `notificationclick`; on `event.action==='reply'` POST `m.room.message` with the stored `{roomId, threadId}`). Ties into N107.
|
||||
|
||||
### [~] P5-30 · Advanced ML Noise Suppression — open verification
|
||||
|
||||
Shipped in the EC fork (DeepFilterNet3 default-capable / DTLN / RNNoise / Speex; AEC on, AGC off for ML tier; never-silent watchdog). **Open:** real-call by-ear **A/B** — model choice, `lotusDenoiseFloor`, AGC on/off (LOTUS_TESTING §D2-1 / J2). **GTCRN (deferred):** tiny MIT 16 kHz model beating RNNoise, but no drop-in browser package — needs `onnxruntime-web` in a Web Worker behind a custom AudioWorklet ring-buffer (ORT can't run in an AudioWorklet, issue #13072); ~1-week build. Revisit only if low-power quality proves insufficient. HW-gated (FRCRN/Maxine) = desktop-Rust-only future.
|
||||
|
||||
### [~] P6-2 · Element Call fork — retire remaining DOM hacks (Phase 2 needs publish)
|
||||
|
||||
Phase 1 shipped: `io.lotus.set_deafen` (LiveKit-source deafen/screenshare-audio-mute) replaces the brittle `<audio>.muted` iframe hack; cinny sends it join-gated alongside the transitional DOM fallback. **Phase 2 (blocked on user npm publish):** publish fork `0.20.1-lotus.2` → bump cinny pin `lotus.1`→`lotus.2` → delete the `CallControl.ts` `.muted` fallback + the EC1–EC6 fixes ship. **Deferred pieces (P6-2b):** the `useCallSpeakers` DOM-scrape is a dormant fallback behind `io.lotus.call_state`; `.click()`-by-`data-testid` UI toggles are low-value fork surface. Divergence to confirm: deafen doesn't silence soundboard/`Unknown`-source audio (setVolume type limit).
|
||||
|
||||
### [~] Mobile audit — code-level pass DONE (device QA + deferred items open)
|
||||
|
||||
Comprehensive **code-level** responsive audit of the LOTUS_FEATURES surface (12 survey agents — 6 area slices + 6 deep per-feature dives — each finding verified, fixed in reviewed batches, then a 5-agent all-files regression+efficacy gate; gate-green tsc/eslint/857 tests/build). Shipped `lotus` commits `d6159997` `836e4a66` `4c298a36` `09415f95` `36fdbdd3` `154e35ef` `09f37f89` (M1–M6 + N1–N2): message-table/composer/call-bar/url-preview/explore overflow fixes; full-screen media/file/avatar viewers + touch-pan for zoomed images; full-screen scrollable member profile (+close btn); native SettingsSelect + tile-body volume sliders + measured GifPicker; full-screen Report/"Seen by" dialogs + full-width toasts + popover clamps; **image/video aspect-ratio (no crop/letterbox on phones)**; 44px room-row + space-rail touch targets. The app was found **structurally sound** on mobile (thread panel, dialogs, drawers, settings shells, ACL/widgets/search/QR/auth all already responsive).
|
||||
|
||||
Intentional desktop deltas (disclosed, non-regressive): volume sliders below labels; Report dialog 380→480px & "Seen by" modals 460→360px (sibling-modal normalization); translate select → folds SettingsSelect.
|
||||
|
||||
**NOT done — needs a real device / product decisions (open):**
|
||||
|
||||
- [ ] **Runtime mobile QA** — none of the above is validated on an actual phone (static analysis only). Needs device/devtools walk-through per LOTUS_TESTING §E.
|
||||
- [x] **Element Call fork in-call mobile UI** — DONE (`element-call:lotus` `e36aef8a`, 3-agent survey + 2-agent review). Fixed the EC iframe's own phone UI: footer control row wraps so hangup can't clip (320–500px), portrait 1:1 self-PiP safe-area inset, 44px camera-flip + reaction-picker targets, settings-tab horizontal scroll, landscape spotlight filmstrip. All mobile-gated (EC is mobile-first CSS). Rides to users on the next fork republish (P6-2). Runtime on-device QA still pending (needs a phone).
|
||||
- [ ] **M2 — touch discoverability** — message quick-reactions/actions are hover-gated; long-press is the fallback but is **unreliable on iOS Safari** (deep audit). A visible touch affordance is needed but the naive fix hides unread badges / clutters messages (member-profile-style redesign).
|
||||
- [~] **Sub-44px touch-target sweep** — primary controls DONE via a shared `MobileTouchTarget` `@media` class (`P1`, `8a1168bc`): in-call bar ×7, call-status bar ×4, thread "N replies" chip, knock Approve/Deny, ACL remove. Secondary batch DONE (`r2`, `72e7447d`): image-viewer close/zoom±/zoom%/download, embed-player Close/Collapse/Fullscreen/View-post, read-receipt "seen by" pill. **Deferred (rationale, not built):** PiP fullscreen/resize handles — enlarging four 24px corners to 44px would swallow a ~160px mobile PiP and block "Return to call" (needs a design rethink, not a blunt bump); presence dot is a non-interactive status indicator (no target needed).
|
||||
- [x] **Avatar-decoration `prefers-reduced-motion`** — DONE (`P2`, `c3e1fbff`): renders just the avatar (no animated APNG overlay) under the preference; no static-frame asset to freeze to.
|
||||
- [x] **Twitch/Twitter/TikTok preview cards** — DONE (`r2`, `72e7447d`). These fragment cards render header/thumbnail beside content as direct children of the `UrlPreview` flex row; added `StackOnMobile` (mobile-only `@media (max-width:750px){ flex-direction:column }`) scoped to those variants via `cardClass`. folds `Box` has no default `direction` so the override wins uncontested; desktop unchanged (verified by 2 review agents). Pre-existing desktop quirk (header bar beside content on Twitter/TikTok at desktop width) left as-is — the fuller fix is wrapping each card body in a column `Box`; out of scope for a mobile pass.
|
||||
- [ ] **M2 — message action/quick-reaction touch discoverability** — hover-gated + iOS-long-press-unreliable; a visible touch affordance collides with unread-badge placement / per-message clutter → needs a design decision + device look.
|
||||
|
||||
### [ ] Inline media embeds — remaining providers (LOW PRIORITY)
|
||||
|
||||
The inline embed system (`videoEmbed.ts`) covers 18 providers (16 + Mixcloud/Deezer); three more were **deliberately deferred** (verified against 2026 docs by review agents):
|
||||
|
||||
- **Bandcamp** (highest-value audio add) — needs an **oEmbed** round-trip: the player URL requires numeric `album`/`track` item ids that aren't in the page URL (`bandcamp.com/oembed` is the resolver; mirror the `TikTokEmbedCard` on-click oEmbed pattern). CSP `frame-src`: `bandcamp.com`. Classify `kind: 'audio'`.
|
||||
- **SoundCloud `on.soundcloud.com` short links** — the `w.soundcloud` widget resolver does **not** follow the redirect; needs the same on-click oEmbed resolve (`soundcloud.com/oembed`, CORS-enabled) to get the canonical URL. (Canonical `soundcloud.com/{user}/{track}` links already work.)
|
||||
- **Vimeo `event/{id}` (live events) + `ondemand/…`** — event embed host is `vimeo.com` (**not** `player.vimeo.com`, so it needs a new CSP `frame-src` host); on-demand is paywalled and doesn't embed for non-purchasers. Low ROI — only do the event case if `vimeo.com` is widened for another reason.
|
||||
|
||||
Also open (from the quality review): a real `onError`/error-state fallback for iframes that fail to load (deleted post / region lock / X login-wall) — cross-origin frames don't fire `onError` reliably, so this needs a load-timeout heuristic; the Close button + badge link are the current escape hatch.
|
||||
|
||||
**✅ Steam detailed embed (2026-07, 2-agent review) — `ef82650c`.** `store.steampowered.com` content URLs get rich cards: **app** pages → OG capsule header + click-to-play facade → Steam's official `/widget/{id}` store iframe (live region-aware price / discount % / Buy on Steam, gated by `inlineMediaEmbeds`); **news/announcement** → banner + headline + body-preview card; **bundle/sub/dlc** → OG store card. `getSteamTarget`/`steamWidgetEmbedUrl` in `videoEmbed.ts` (+tests). Grounded in prod CSP (`frame-src https:` allows the widget with no infra change; images via homeserver `mxc`; NO client-side Steam API — `connect-src` + Steam CORS both block it, which is the honest ceiling: no review scores/genres/screenshots client-side). **Needs on-device QA:** the live widget iframe height/fit (can't render headlessly) — verify the price/Buy stay visible on desktop-wide and phone.
|
||||
|
||||
**✅ GIF previews now animate + Mixcloud/Deezer embeds (2026-07, 2-agent review) — `4154cae5`.** Reported live: a `media.giphy.com` link "shows the gif's image but doesn't play it." Root cause: **Synapse's `/thumbnail` endpoint flattens animated GIFs to a still first frame**, and every preview image went through it. `GifCard` (Giphy/Tenor) + the generic OG card now request the **original** via `/download` (`mxcUrlToHttp` with no width/height) when the preview is a GIF (`og:image:type === 'image/gif'` or a `.gif` pathname). Guarded: `shouldServeGifOriginal()` keeps the frozen thumbnail past a **10 MB** `matrix:image:size` cap, and the generic card's eager `<img>` gained the `loading="lazy"` it was the only preview image missing. Also added **Mixcloud + Deezer** audio embeds, and fixed Deezer podcasts (they live at `/show/<id>`, **not** `/podcast/<id>` — the latter 404s on Deezer's own oEmbed; verified against the live API). **Needs on-device QA:** confirm a large GIF still animates and doesn't stall the timeline.
|
||||
|
||||
**✅ Embed bug hunt (2026-07, 3 survey agents + 2-agent review) — `f2673eff`.** Core posture verified **sound** (iframe sandbox, `useIframeAutoHeight` postMessage origin+source trust, no XSS/`dangerouslySetInnerHTML`, `rel="noreferrer"` on all 21 links, oEmbed no-SSRF, the whole facade→iframe/abort/observer lifecycle). Fixed: Twitch/Kick/SoundCloud/Streamable reserved-path over-match (utility pages rendered as broken players), Vimeo hash over-capture (`[0-9a-f]{6,}`), Spotify/Steam/Discord/IMDb `og:image` now via `mxcUrlToHttp` (was a broken raw `mxc://` `<img>` + a pre-click 3p-request facade bypass), `wide` class follows the og:url-resolved embed, Twitter host alignment (`mobile.twitter.com`/`/statuses/`), URL de-dupe.
|
||||
|
||||
**Deferred / surfaced from the hunt (not fixed — decide before doing):**
|
||||
|
||||
- **Security-vs-functionality tradeoff (needs a call):** drop `allow-popups-to-escape-sandbox` and/or `clipboard-write` from `EMBED_SANDBOX`/`allow=` on embed iframes — real hardening against a _compromised_ provider (phishing popup / clipboard hijack), but risks breaking a legit provider popup/copy on the trusted major providers we embed. Low marginal value; not shipped blindly.
|
||||
- **Defense-in-depth:** `encodeURIComponent` the Bluesky authority + Apple Music path/search interpolated into the embed `src` (not currently exploitable — host is fixed and value comes from `URL.pathname`; React escapes the attribute).
|
||||
- **Out of embed scope (real, low-sev):** `LotusDenoiseFeature` (`ClientNonUIFeatures.tsx`) has a `window` `message` listener with **no origin/source check** → any frame/window can post `{type:'lotus-denoise-status', error}` and pop a forged **"System"** toast (text only, no XSS). Validate `event.source`.
|
||||
- **Lifecycle Lows (cosmetic/latent):** a re-fetch flips a playing embed back to the spinner (latent — url is keyed); auto-height retained across close→reopen; `extractEmbedHeight` generic `.height` fallback accepts any allowed-origin message; `TweetEmbed` theme is a one-time `matchMedia` snapshot (no live theme switch); host-normalization gaps (`vt.tiktok.com` misses `StackOnMobile`, `m.instagram.com`, `www.youtu.be`).
|
||||
|
||||
### Deferred / dropped (decided — kept for context)
|
||||
|
||||
- **[DEFERRED] P5-51** Federated "Identity Contexts" (session isolation) — multi-sprint, touches auth/crypto/storage core; smaller intermediate step = plain multi-account switch. **[DROPPED] P5-52** per-room sync governor — js-sdk can't truly per-room filter `/sync`; only a cosmetic hide. **[DEFERRED] P5-53** local scripting plugin — prefer a declarative automation-rules feature (no arbitrary code). **[DEFERRED] Audit-3** profile banner — MSC4427 open/unmerged; revisit on merge. **[WON'T FIX] P5-50** Windows HW media pipeline (WebRTC decode lives in WebView2; not injectable). **[MOVED] P5-9** LFG → LotusBot `!lfg`.
|
||||
|
||||
---
|
||||
|
||||
## 🚫 Blocked Features (server / upstream gated)
|
||||
|
||||
Re-run `/_matrix/client/versions` + `unstable_features` after each Synapse upgrade. **Re-checked on 1.157.1 (2026-07-23): no change — all four below are still `false`.** The 1.156.0→1.157.1 delta unblocked nothing (it's a bugfix release; the only feature-bearing release in the gap was 1.156.0, which we were already running).
|
||||
|
||||
- **[BLOCKED] Live Location Sharing** (MSC3489 + MSC3672 both `false`) — real-time GPS beacons over the existing static share.
|
||||
- **[BLOCKED] Reaction/Relation Redaction** (MSC3892 `false`) — remove a reaction without redacting the parent; current full-redaction fallback is acceptable.
|
||||
- **[DONE 2026-07] Room Preview before joining** (MSC3266) — the client was always built (`JoinBeforeNavigate` → `RoomCard` via `mx.getRoomSummary`). The earlier "blocked" flag was a **misdiagnosis**: it tested `/v1/rooms/{id}/summary` (404), but the SDK calls the _unstable_ `im.nheko.summary/summary/{id}` path, which returns **200** with name/topic/members/join_rule. Verified live after the 1.156 upgrade; also added a join-rule/encryption chip + Request-to-join for knock rooms to the preview card.
|
||||
- **[BLOCKED] Thread Subscriptions** (MSC4306 `false`) — "Follow thread" button (depends on the shipped Thread Panel).
|
||||
|
||||
---
|
||||
|
||||
## 📖 Reference
|
||||
|
||||
### Server Capabilities (as of 2026-07)
|
||||
|
||||
- **Homeserver** `matrix.lotusguild.org` · **Synapse** `1.157.1+trixie1` (upgraded 2026-07-23 from **1.156.0** — note the host was found on 1.156.0 while the docs claimed 1.155.0, so **always verify with `dpkg-query -W matrix-synapse-py3`**, don't trust the docs; apt package on Debian 13, LXC 151) · **Matrix spec** up to `v1.12` (Synapse still advertises v1.12; MSC features via `unstable_features`).
|
||||
- **MSC ON** (re-dumped live from `/_matrix/client/versions` on 1.157.1): `msc4140` · `msc3771` · `msc3440.stable` · `msc4133.stable` · `simplified_msc3575` · `msc4222` · `msc3266` (room summary live at unstable `im.nheko.summary/summary/{id}` — 200; the `/v1/rooms/{id}/summary` path is still 404) · `msc3401_matrix_rtc` · `msc2285.stable` · `msc3827.stable` · `msc3981` · `msc4380.stable` · `msc4445` · `msc2659.stable` · `msc2666` · `msc2432` · `e2e_cross_signing` · `label_based_filtering`. **OFF/blocked:** `msc4306` · `msc3882` · `msc3912` · `msc4155` · `msc3489`/`msc3672` · `msc3892` · `msc4028` · `msc4069` · `msc4108` · `msc3391` · `msc4354` (sticky events — **deliberately off**, see the Matrix 2.0 section above) · `msc4143` (RTC foci — **not a gap**: LiveKit is discovered via `.well-known` `org.matrix.msc4143.rtc_foci`, confirmed live, not this flag).
|
||||
- **Dead client code:** Synapse 1.157.0 **removed** `msc3861` (MAS auth delegation) entirely — the ~6 `msc3861`/`msc2965` references in `src/` can never activate against this homeserver (we auth via Authelia `oidc_providers`). Harmless, but cleanup material.
|
||||
- **Live endpoints:** Report User (MSC4260) **200** ✅ · Report Room (MSC4151) ✅.
|
||||
- **Homeserver access (audits):** Synapse = LXC 151 (`pct exec 151 -- bash`), config `/etc/matrix-synapse/homeserver.yaml`. Web deploy = LXC 106. Voice guard = `voice-limit-guard.py` on LXC 151.
|
||||
- **SDK notes:** no arbitrary profile-field methods (use `mx.http.authedRequest()` for MSC4133); js-sdk can't per-room filter `/sync`; sanitizer strips `<math>`/MathML; SW exists at `src/sw.ts`; `getMatrixToRoom()` builds invite URLs; EC audio-inject unblocked via the fork's `io.lotus.inject_audio`.
|
||||
|
||||
### Key File Reference
|
||||
|
||||
| What | File | Lines |
|
||||
| ------------------------------ | ------------------------------------------------------------------- | ------------------- |
|
||||
| Global keydown / room nav | `hooks/useKeyDown.ts` · `hooks/useRoomNavigate.ts` | whole / 19-72 |
|
||||
| Room unread counts atom | `state/room/roomToUnread.ts` | `roomToUnreadAtom` |
|
||||
| Overlay portal provider | `pages/App.tsx` · `index.html` | 65 / 101 |
|
||||
| Room settings tabs | `features/room-settings/RoomSettings.tsx` | 27-56 |
|
||||
| State event read/write pattern | `features/common-settings/general/RoomEncryption.tsx` | 42-52 |
|
||||
| Power levels | `hooks/usePowerLevels.ts` | whole |
|
||||
| Slash commands | `hooks/useCommands.ts` | 140-537 |
|
||||
| Chat background picker/defs | `features/settings/general/General.tsx` · `lotus/chatBackground.ts` | 945-981 / whole |
|
||||
| Matrix.to URL builder | `plugins/matrix-to.ts` | `getMatrixToRoom()` |
|
||||
| Media URL conversion | `utils/matrix.ts` | `mxcUrlToHttp()` |
|
||||
| Search pagination / virtual | `features/message-search/{useMessageSearch,MessageSearch}.tsx` | 74-121 / 234-365 |
|
||||
| Call mic control | `plugins/call/CallControl.ts` | 206-212 |
|
||||
| Knock support check | `utils/matrix.ts` | 376-391 |
|
||||
| Notification mute push rules | `hooks/useRoomsNotificationPreferences.ts` | 110-150 |
|
||||
|
||||
### Element Call fork — operational reference
|
||||
|
||||
Fork = `LotusGuild/element-call` (branch `lotus`, upstream base **v0.25.0** since the 2026-09 sync — was v0.20.1); cinny consumes the npm package `@lotusguild/element-call-embedded` (built bundle copied into `public/element-call/`).
|
||||
|
||||
**Toolchain (upstream-driven, accepted 2026-09):** Node ≥ 22.13 (`.node-version` = 24) and **pnpm 11**, installed directly (`npm i -g pnpm@<packageManager version>`, currently 11.21.0) — **not** via `corepack enable`: `matrix-js-sdk` is a git dependency pnpm builds from source, and its own devEngines pins pnpm 11.9.0; a corepack-shimmed pnpm refuses to switch for that nested install and `pnpm install` fails (fork CI run #1854). pnpm 10 rejects the lockfile and Node 20 cannot build. Lint is **oxlint + oxfmt** (upstream dropped eslint/prettier in v0.25.0): `pnpm lint` (tsc + oxlint + knip) and `pnpm format:check` / `pnpm format`. `matrix-js-sdk` is pinned to a `matrix-org/matrix-js-sdk#develop` commit in the lockfile, as upstream ships it. Fork CI (`.gitea/workflows/ci.yml`) hard-gates lint + format + `pnpm test:unit` before build, with `concurrency: cancel-in-progress`.
|
||||
|
||||
**Publish a new version (CI on tag push; needs the `NPM_PUBLISH_TOKEN` org secret):** the published version is derived from the git tag — bump `embedded/web/package.json` (currently `0.25.0-lotus.3`, published by CI; the secret is `NPM_PUBLISH_TOKEN`, names starting `GITEA_` are reserved), push `lotus`, then `git push lotus v0.25.0-lotus.1`; the `publish` job builds and publishes to the Gitea registry. Always push (never delete) the annotated `vX.Y.Z-lotus.N` tag for every published version. Then in cinny bump the `@lotusguild/element-call-embedded` pin (currently `0.25.0-lotus.3`) → `npm install` → build. Manual fallback: `pnpm run build:embedded && cd embedded/web && npm version <ver> --no-git-tag-version && npm publish`.
|
||||
|
||||
**`io.lotus.*` widget actions** (add new toWidget actions to the enum + `LOTUS_TO_WIDGET_ACTIONS` in `src/lotus/lotusActions.ts`; only send AFTER call-join or a 10s timeout fires):
|
||||
|
||||
| Action | Dir | Purpose | Module |
|
||||
| :--------------------------- | :------ | :----------------------------------------------------- | :-------------------- |
|
||||
| `io.lotus.call_state` | EC→host | speaker/mute/camera stream (`lotusCallState=1`) | `lotusCallState.ts` |
|
||||
| `io.lotus.focus_participant` | host→EC | spotlight (works during screenshare) | `lotusFocus.ts` |
|
||||
| `io.lotus.inject_audio` | host→EC | soundboard clip mixed into call (`lotusAudioInject=1`) | `lotusAudioInject.ts` |
|
||||
| `io.lotus.set_quality` | host→EC | audio/screenshare bitrate/fps caps | `lotusQuality.ts` |
|
||||
| `io.lotus.decorations` | host→EC | in-call avatar decorations | `lotusDecorations.ts` |
|
||||
| `io.lotus.set_deafen` | host→EC | LiveKit-source deafen (P6-2) | `lotusDeafen.ts` |
|
||||
|
||||
Also flag-gated: `lotusTransparent`/`lotusTheme`, `lotusDenoiseSource=1` (in-source ML denoise).
|
||||
|
||||
### CI/CD + per-feature checklist
|
||||
|
||||
```
|
||||
edit → commit → git push origin lotus
|
||||
→ Gitea Actions (.gitea/workflows/ci.yml): npm ci → build + npm test + tsc + eslint + prettier (ALL hard gates) → audit + bundle-size (informational)
|
||||
→ lotus_deploy.sh on LXC 106 polls the "Build & Quality Checks" status → npm ci && npm run build → rsync → live (~11 min)
|
||||
(a push that lands while a deploy is mid-build is queued and deployed right after — matrix@b6ea4a3; before that it was dropped)
|
||||
```
|
||||
|
||||
Before marking a feature complete: `npx tsc --noEmit` (0 errors) · `npx eslint src/` (0 new) · `npx prettier --check src/` · `npm test` (Node runner via tsx, hard CI gate — colocated `*.test.ts`) · update `README.md`/`landing/index.html` for Lotus-custom features · visually verify on `chat.lotusguild.org`.
|
||||
|
||||
**CI hardening (2026-07, reviewed):**
|
||||
|
||||
- [x] **Concurrency** — `cancel-in-progress` on cinny `ci.yml` and cinny-desktop `release.yml` (`386a2979` / `c5461ce`): a superseded lotus push cancels its in-flight web CI and collapses queued ~30-min Tauri desktop builds to just the newest. Safe for deploys because `lotus_deploy.sh` now **follows origin/lotus HEAD** each poll iteration + resets to the gated SHA (`matrix` `c15a489`) — closes the latched-SHA freeze race.
|
||||
- [x] **Hard quality gates** — typecheck/eslint/prettier promoted from `continue-on-error` to blocking (tree held clean). eslint gates on errors only; `no-explicit-any` warnings stay informational.
|
||||
|
||||
**CI follow-ups (open):**
|
||||
|
||||
- [ ] **Dedicated `desktop-linux` runner** (infra) — concurrency only collapses _burst_ stacking; a single in-flight `build-linux` (Tauri, `ubuntu-latest`) still shares the runner with web CI and can queue a web CI/deploy up to ~30 min. Fix = register a 2nd Linux act_runner labelled `desktop-linux` (root, network, RAM for a Tauri build; do NOT also label it `ubuntu-latest`) and point only `build-linux: runs-on` at it. Relabeling without a matching runner hangs the job forever.
|
||||
- [ ] **Debounce the desktop trigger** — `trigger-desktop` fires a full desktop build on _every_ lotus commit; consider tag/`workflow_dispatch`/schedule-gating to decouple desktop cadence from web commits (biggest remaining runner-load source).
|
||||
- [ ] **Verify Gitea ≥ 1.24** actually honors workflow `concurrency` (older silently ignores it → safe no-op, but the change is then inert — confirm on a test burst).
|
||||
- [ ] **Deferred (chosen-not-now):** build-once/deploy-the-artifact (kill the CI-then-deploy double build).
|
||||
- [x] **CI-gate the `lotus-build.sh` upstream-merge path** — DONE (matrix repo, cinny#98): the script now merges, runs the local gates (npm ci, typecheck, eslint, prettier, tests), and pushes; CI + `lotus_deploy.sh` deploy exactly like any other lotus commit. A failed gate leaves the merge local (not pushed).
|
||||
@@ -174,7 +174,7 @@ Beyond the web client, the desktop app adds native OS integration (Windows-focus
|
||||
|
||||
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.
|
||||
See [LOTUS_FEATURES.md](LOTUS_FEATURES.md) for the full feature changelog and [LOTUS_REFERENCE.md](LOTUS_REFERENCE.md) for the design laws and operational reference (open work is in [Gitea issues](https://code.lotusguild.org/LotusGuild/cinny/issues)).
|
||||
|
||||
### Local Development
|
||||
|
||||
@@ -209,7 +209,7 @@ avatar decorations on EC video tiles, and a native transparent background.
|
||||
(`io.lotus.set_quality`).
|
||||
|
||||
The fork's `io.lotus.*` action catalog + the publish procedure are in
|
||||
**[`LOTUS_TODO.md`](LOTUS_TODO.md)** ("Element Call fork — operational reference");
|
||||
**[`LOTUS_REFERENCE.md`](LOTUS_REFERENCE.md)** ("Element Call fork — operational reference");
|
||||
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.
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ try {
|
||||
// patched form was found, meaning folds changed the Icon implementation.
|
||||
// Fail loudly so the postinstall hook / CI breaks instead of silently
|
||||
// shipping an unpatched folds (which crashes at render with "src is not a
|
||||
// function"). See LOTUS_TODO.md "Dependencies / Build / Hygiene" for
|
||||
// function"). See cinny #210 (build hygiene notes) for
|
||||
// context on why this is a direct node_modules patch rather than
|
||||
// patch-package.
|
||||
console.error('ERROR: folds Icon patch target not found.');
|
||||
@@ -52,7 +52,7 @@ try {
|
||||
console.error(` Expected to find (surrounding context):\n${original}`);
|
||||
console.error(
|
||||
' folds likely changed its Icon implementation. Update the patch target ' +
|
||||
'in scripts/patch-folds.mjs (see LOTUS_TODO -> "Dependencies / Build / Hygiene" ' +
|
||||
'in scripts/patch-folds.mjs (see cinny #210 ' +
|
||||
'-> patch-folds.mjs entry) before building.',
|
||||
);
|
||||
process.exit(1);
|
||||
|
||||
@@ -41,7 +41,7 @@ type RoomQualityProps = {
|
||||
/**
|
||||
* [P5-31] Room-admin quality ceiling. Writes `io.lotus.room_quality`; every
|
||||
* Lotus client clamps its per-user quality to these caps. Hard enforcement for
|
||||
* ALL Matrix clients is a server-side follow-up (see LOTUS_TODO.md P5-31).
|
||||
* ALL Matrix clients is a server-side follow-up (see cinny #174, D2-8).
|
||||
*/
|
||||
export function RoomQuality({ permissions }: RoomQualityProps) {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
@@ -8,7 +8,7 @@ import { CallAudioBitrate, ScreenshareBitrate, ScreenshareFramerate } from '../s
|
||||
*
|
||||
* NOTE: the client applies these as a best-effort UX cap. Hard enforcement for
|
||||
* ALL Matrix clients is a server-side follow-up (a `voice-limit-guard`-style
|
||||
* sidecar on LXC 151 that reads this event) — see LOTUS_TODO.md P5-31.
|
||||
* sidecar on LXC 151 that reads this event) — see cinny #174 (D2-8).
|
||||
*/
|
||||
export type RoomQualityContent = {
|
||||
// Numeric caps: client-cooperative only (our fork honors them; the SFU cannot
|
||||
|
||||
@@ -5,7 +5,7 @@ import pkg from '../../../package.json';
|
||||
//
|
||||
// Installs pass-through wrappers around `console.warn` / `console.error` that
|
||||
// ring-buffer any log line matching the KE-1..KE-4 bug-cluster signatures
|
||||
// (E2EE KE-1..4 capture; see LOTUS_TODO.md). It NEVER swallows a log call — the
|
||||
// (E2EE KE-1..4 capture; see cinny #201). It NEVER swallows a log call — the
|
||||
// original console method is always invoked — and it performs NO network I/O.
|
||||
// The report metadata is limited to SDK version / device id / user id / sync
|
||||
// state; the captured log lines themselves are intentional evidence and may
|
||||
|
||||
Reference in New Issue
Block a user