docs: P3-4 accessibility — features section, TODO/BUGS, LOTUS_TESTING §P

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 11:45:22 -04:00
parent 6728a1274d
commit 899a14c119
6 changed files with 126 additions and 66 deletions
+2 -1
View File
@@ -16,7 +16,7 @@ step-by-step checks in [`LOTUS_TESTING.md`](./LOTUS_TESTING.md).
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 |
@@ -41,6 +41,7 @@ Implemented and gate-green; confirm each per `LOTUS_TESTING.md`, then delete the
| 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 |
**Verified working in live testing (2026-06):** A2, B1B4, C1, C3, D (mic/camera/deafen/screenshare/fullscreen/more-menu/PiP). Denoise quality in D is still poor — tracked under the denoise project, not a regression.
+17 -12
View File
@@ -18,7 +18,7 @@
From the matrix infra README (`/root/code/matrix/README.md`):
| Thing | Value |
|-------|-------|
| ------------------------ | ------------------------------------------------------------- |
| Synapse host | LXC **151**, `10.10.10.29` (Synapse 1.155.0) |
| Synapse log | `/var/log/matrix-synapse/homeserver.log` |
| Synapse config | `/etc/matrix-synapse/homeserver.yaml` (+ `conf.d/`) |
@@ -86,6 +86,7 @@ use it as the primary client artifact and DevTools as the raw backup.
/var/log/matrix-synapse/homeserver.log | grep "<user_id>"
```
- **Synapse SQL (LXC 109) — what the server thinks it holds:**
```sql
-- Current OTK inventory for the device (compare key_id set against the
-- request body the client keeps retrying).
@@ -106,8 +107,10 @@ use it as the primary client artifact and DevTools as the raw backup.
FROM e2e_fallback_keys_json
WHERE user_id = '@user:matrix.lotusguild.org' AND device_id = '<DEVICE_ID>';
```
> Table names are Synapse 1.155 (`e2e_one_time_keys_json`,
> `e2e_fallback_keys_json`). If a name is absent, list with `\dt e2e*` in psql.
- **Confirms:** if the offending `key_id` (from the 400) is **present** in
`e2e_one_time_keys_json` with a **different** stored value than the client's
request body → OTK state has diverged (rust-crypto store vs Synapse). That is
@@ -132,6 +135,7 @@ use it as the primary client artifact and DevTools as the raw backup.
/var/log/matrix-synapse/homeserver.log | grep -E "<user_id>|<participant_id>"
```
- **Synapse SQL (LXC 109) — undelivered / queued to-device events:**
```sql
-- Backlog of to-device messages queued for the affected device. A growing
-- count here = the HS has the media-key events but the device isn't draining
@@ -145,6 +149,7 @@ use it as the primary client artifact and DevTools as the raw backup.
SELECT device_id, display_name, last_seen, ts
FROM devices WHERE user_id = '@user:matrix.lotusguild.org';
```
- **Confirms:** to-device events present but undecryptable (client shows the
`io.element.call.encryption_keys` "unexpected encrypted" warning) ⇒ there is
**no valid Olm session** to decrypt them — the expected downstream of KE-1.
@@ -265,7 +270,7 @@ Q4. KE-4: do delayed-event timeouts coincide with Synapse p99 latency spikes
> "fix" before the planning session** — they are listed so evidence collection
> can be paired with a recovery plan. Confirm the root condition (Q1/Q2) first.
1. **Per-device logout + re-login of the affected device** *(lowest blast radius)*
1. **Per-device logout + re-login of the affected device** _(lowest blast radius)_
- **What:** log the one glitching device out and back in. Forces a fresh
device id, fresh device keys, and a clean OTK batch — sidesteps a diverged
OTK store without touching other sessions.
@@ -275,7 +280,7 @@ Q4. KE-4: do delayed-event timeouts coincide with Synapse p99 latency spikes
- **Confirms/uses:** if KE-1 stops after this, OTK-store divergence (Q1) was
the cause.
2. **Client crypto-store reset (`clearLoginData` path)** *(medium)*
2. **Client crypto-store reset (`clearLoginData` path)** _(medium)_
- **What:** `clearLoginData()` in `src/client/initMatrix.ts` (coordinator's
file — do not edit) **deletes ALL IndexedDB databases** (incl.
`web-sync-store` and the rust-crypto store `crypto-store`), **unregisters
@@ -294,16 +299,16 @@ Q4. KE-4: do delayed-event timeouts coincide with Synapse p99 latency spikes
- **Use when:** the rust-crypto store itself is corrupt/diverged and option 1
didn't clear it.
3. **SDK pin change off the RC** *(medium — codebase change, needs rebuild)*
3. **SDK pin change off the RC** _(medium — codebase change, needs rebuild)_
- **Current pin:** `package.json` → `"matrix-js-sdk": "41.6.0-rc.0"` (a
release candidate).
- **Finding (npm / GitHub changelog, checked 2026-07):** stable **`41.6.0`**
was released **2026-05-26**. Its only changelog line is *"Throw sane error
on completeLoginOnNewDevice IdP rejection"* — **no OTK / keys-upload / Olm /
was released **2026-05-26**. Its only changelog line is _"Throw sane error
on completeLoginOnNewDevice IdP rejection"_ — **no OTK / keys-upload / Olm /
to-device fix** relative to the RC. Later stable lines exist
(`41.7.0`, `41.8.0`; `41.7.0-rc.3` / `41.9.0-rc.0` seen as pre-releases).
Nearby crypto-relevant entries: `41.5.0` *"Enable encrypted history sharing
by default"*; `41.4.0` key-backup handling. **No changelog entry directly
Nearby crypto-relevant entries: `41.5.0` _"Enable encrypted history sharing
by default"_; `41.4.0` key-backup handling. **No changelog entry directly
addresses the KE-1 OTK-conflict symptom** in the immediate range — so
moving RC→`41.6.0` stable is a low-risk hygiene step but is **not expected
to fix KE-1 by itself**. Before pinning, re-read the CHANGELOG for any
@@ -312,7 +317,7 @@ Q4. KE-4: do delayed-event timeouts coincide with Synapse p99 latency spikes
rust-crypto IndexedDB schema — a downgrade triggers the `IDB_VERSION_CONFLICT`
path in `initMatrix.ts`.
4. **Synapse-side OTK row surgery** *(LAST RESORT — highest danger)*
4. **Synapse-side OTK row surgery** _(LAST RESORT — highest danger)_
- **What:** deleting/rewriting rows in `e2e_one_time_keys_json` (and/or
`e2e_fallback_keys_json`, `device_inbox`) for the affected device to force
the client to re-upload a clean batch.
@@ -324,7 +329,7 @@ Q4. KE-4: do delayed-event timeouts coincide with Synapse p99 latency spikes
- **Guardrails if ever done (planning session + HS owner only):** full
`pg_dump` of `synapse` first; do it during **zero active calls**; delete only
the exact diverged `key_id` for the exact `device_id`; `systemctl restart
matrix-synapse` to flush caches; then log the device out/in (option 1) so it
matrix-synapse` to flush caches; then log the device out/in (option 1) so it
republishes. **Never** run this speculatively.
---
@@ -337,7 +342,7 @@ Do these **in order**. Aim to have client + server capturing the **same call**.
`tail -F /var/log/matrix-synapse/homeserver.log | tee /tmp/lotus-call-$(date +%s).log`.
(Optionally raise the `synapse.rest.client.keys` / `handlers.e2e_keys` /
`handlers.devicemessage` loggers to DEBUG per §0 and `systemctl reload
matrix-synapse` — remember to revert after.)
matrix-synapse` — remember to revert after.)
2. **Prep client:** open Lotus Chat → Settings → Developer Tools → **enable
Developer Tools** so the **Crypto Diagnostics** card is visible; note its
entry count starts at (or reset by reload to) 0.
@@ -373,7 +378,7 @@ Do these **in order**. Aim to have client + server capturing the **same call**.
with pass-through wrappers (originals always called) that ring-buffer (max
**200**) any line matching the KE signatures. No network, no timers.
- `getCryptoDiagEntries()` — snapshot copy of the buffer (`{ ts, level, ke,
signature, message }`, most-recent-last).
signature, message }`, most-recent-last).
- `buildCryptoDiagReport(mx)` — JSON string: SDK version, device id, user id,
sync state, `cryptoReady` (`mx.getCrypto()` presence), per-KE counts, and the
entry buffer. No tokens/PII beyond those ids; captured log lines are retained
+12
View File
@@ -1179,6 +1179,18 @@ Three one-tap presets at the top of **Settings → Notifications** that apply a
---
## Accessibility (P3-4)
WCAG 2.1 AA hardening of the golden path (find room → read → reply → send) for keyboard and screen-reader users.
- **Timeline for screen readers:** each message is `role="article"`; **collapsed messages announce their sender + time** (they drop the visible header, so AT would otherwise hear the body with no attribution). The timeline is a `role="log"` `aria-live="polite"` region so new messages are announced; emoji/emoticons carry text labels.
- **Live status:** typing indicators announce via a `role="status"` region; editing a message announces "Editing message from <sender>".
- **Forms & overlays:** all inputs have associated labels (visible `<label htmlFor>` or `aria-label`); the Media Gallery and Search overlays are named.
- **Focus management:** skip-to-content link + `nav`/`main` landmarks; genuine dialogs return focus to their trigger on close (inline popouts intentionally keep focus in context).
- **Keyboard-shortcuts help:** press <kbd>?</kbd> for a dialog of the existing shortcuts (Escape, type-to-focus composer, Enter/Shift+Enter send, message actions).
- **Regression gate:** a curated `eslint-plugin-jsx-a11y` rule set (ARIA correctness + label association) runs in CI. Files: `components/message/*`, `features/room/RoomViewTyping.tsx`, `features/shortcuts/*`, `utils/a11y.ts`, `eslint.config.mjs`.
- _Known limitation:_ list virtualization keeps far-scrolled history out of the a11y tree (perf trade-off); newly-arriving messages are announced.
## Infrastructure
### Authenticated Media
+32
View File
@@ -630,6 +630,38 @@ We shipped the diagnostics kit + a **Crypto Diagnostics** card (**Settings → D
---
## 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).
---
## 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.
+15 -5
View File
@@ -141,7 +141,12 @@ Status: `[ ]` pending · `[~]` in progress · `[x]` completed
## Priority 3 — Higher complexity / lower daily frequency
### [ ] P3-4 · Accessibility Improvements (WCAG 2.1 AA)
### [~] P3-4 · Accessibility Improvements (WCAG 2.1 AA) — COMPLIANCE PASS DONE (2026-07), ⚠️ AWAITING LIVE AXE/SR AUDIT
**Shipped (compliance + shortcuts-help tier):** messages `role="article"` + collapsed-message sender/time announced to AT (the biggest gap — collapsed rows had no sender for a screen reader); ~10 unlabeled form inputs + Media Gallery / Search overlays named; emoji/emoticon aria-labels; typing indicator now announced via a `role="status"` live region; editing a message announces "Editing message from X"; focus now returns to the trigger on close of 4 genuine dialogs (RoomIntro/Reactions/RoomViewHeader-topic/Search — inline popouts correctly left); a `?` keyboard-shortcuts help dialog; and a **jsx-a11y lint gate** (curated ARIA-correctness + label rules, enforced in CI) to prevent regressions. Already-good before this pass: skip link + landmarks, timeline `role="log"`/`aria-live`, ~99% icon-button labels, labeled editor.
**DEFERRED (documented):** virtualization keeps scrolled-away history out of the a11y tree (architectural; the live-region announces newly-arriving messages) — not re-architected to avoid perf regression; roving-tabindex + command palette + section-jump shortcuts (user-deferred); the live axe-core / VoiceOver+NVDA audit → LOTUS_TESTING §P.
_Original scope (for reference):_
**What:** Comprehensive audit and fix pass targeting the critical user paths:
@@ -167,13 +172,14 @@ Status: `[ ]` pending · `[~]` in progress · `[x]` completed
Built per the design below (4-agent build + 2-agent review). Gates green (tsc/eslint/build/tests). **Release note: threaded replies no longer render inline in the main timeline — roots show a "N replies" chip that opens the panel.**
**Manual QA checklist (post-deploy):**
1. Reply in Thread (message menu) → panel opens; send text/upload/emoji into it (appears pending → confirmed)
2. Reply to a reply inside the panel → event carries `m.thread` + `m.in_reply_to` with `is_falling_back:false`
3. Main timeline: root + chip only (replies absent); chip count/time updates live; unread badge appears for others' thread replies and clears after viewing the panel
4. Room badge clears via normal markAsRead even with unread threads (unthreaded receipt)
5. Reload: partitioning persists; encrypted-room threads decrypt (back-pagination too)
6. Escape / × closes; mobile = fullscreen panel; switching rooms and back restores the open thread
**What:** A right-side drawer for threaded conversations. Currently "Reply in Thread" exists but there is no panel to read or write thread replies.
**What:** A right-side drawer for threaded conversations. Currently "Reply in Thread" exists but there is no panel to read or write thread replies.
Features:
@@ -218,13 +224,14 @@ Features:
**Shipped (Slack-style):** default = **Participating** (notified only for threads you've posted in or where you're @mentioned); per-thread override **All / Mentions-only / Mute** via the bell menu in the thread panel header; modes sync across devices (`io.lotus.thread_notifications` account data, pruned on write). Mute also suppresses the chip badge and subtracts the thread from the room's sidebar badge (client-side). Also fixed the underlying path: thread replies are notified via exactly one handler (room-level `ThreadEvent.NewReply`), with the main-timeline notifier + unread binder thread-guarded, and live badge refresh on `RoomEvent.UnreadNotifications`.
**Manual QA checklist (post-deploy):**
1. Friend replies in a thread YOU posted in → notification + sound; in a thread you never touched → silent (chip badge only)
2. @mention in any thread → notified regardless of participation
3. Set a thread to Mute → no notifications, chip badge gone (bell-mute glyph), room sidebar badge drops by that thread's count
4. Set to All → every reply notifies; Mentions-only → only @mentions
5. Second device shows the same per-thread modes (account-data sync)
6. Room-level Mute still silences everything incl. thread overrides
**Known caveats:** Mentions-only can under-notify in E2EE rooms (decision runs pre-decryption — same class as the existing notifier); muted-thread badge subtraction is Lotus-only (other clients still count them).
**Known caveats:** Mentions-only can under-notify in E2EE rooms (decision runs pre-decryption — same class as the existing notifier); muted-thread badge subtraction is Lotus-only (other clients still count them).
---
@@ -417,10 +424,11 @@ Features:
**Decision:** Deferred (reviewed 2026-07). Multi-sprint, and it touches the auth/crypto/storage core — not worth the risk/effort for a niche need right now. Kept here with a concrete spec so it's actionable later.
**Future-work spec (why it's big):** the app is currently **single-session**.
- Session lives in `src/app/state/sessions.ts` under fixed localStorage keys — `cinny_access_token`, `cinny_device_id`, `cinny_user_id`, `cinny_hs_base_url`, plus the OIDC keys (`cinny_refresh_token`, `cinny_expires_at`, `cinny_oidc_*`).
- Persistence lives in `src/client/initMatrix.ts`: two fixed IndexedDB stores — `web-sync-store` (`IndexedDBStore`) and `crypto-store` (`IndexedDBCryptoStore`) — feeding one `createClient(...)`.
True per-context isolation would require: (1) namespace every localStorage key per context (`ctx:<id>:cinny_*`); (2) per-context IndexedDB dbNames for **both** the sync store and the crypto store; (3) a context registry + switcher UI (create/rename/delete/switch); (4) full client teardown + re-init on switch (`initMatrix` currently assumes one global client); (5) per-context settings + notification/quiet-hours state; (6) careful crypto-store isolation so device keys never bleed across contexts. **Smaller intermediate step** if demand appears: plain multi-account (fast account switch) *without* the hard isolation boundary — much less risky, reuses most of the login flow.
True per-context isolation would require: (1) namespace every localStorage key per context (`ctx:<id>:cinny_*`); (2) per-context IndexedDB dbNames for **both** the sync store and the crypto store; (3) a context registry + switcher UI (create/rename/delete/switch); (4) full client teardown + re-init on switch (`initMatrix` currently assumes one global client); (5) per-context settings + notification/quiet-hours state; (6) careful crypto-store isolation so device keys never bleed across contexts. **Smaller intermediate step** if demand appears: plain multi-account (fast account switch) _without_ the hard isolation boundary — much less risky, reuses most of the login flow.
**Priority:** Extreme Low (Multi-sprint/Architectural).
### [DROPPED] P5-52 · Desktop — Room-Level Sync Governor (Performance Control)
@@ -510,7 +518,7 @@ Exhaustive, low-level implementation details for backlog items. Follow these pat
**Decisions (each backed by SDK evidence in node_modules/matrix-js-sdk):**
| Question | Decision |
|---|---|
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Thread rendering | **New lean `ThreadTimeline`** reusing `Message`, `useVirtualPaginator`, and RoomTimeline's exported timeline helpers (lines 156-227). Do NOT refactor 2214-line RoomTimeline (its ~35 hooks are hardwired to the room live timeline). |
| threadSupport | **Enable `threadSupport: true`** in `initMatrix.ts` (~line 39). ⚠️ Thread replies then LEAVE the main timeline (`room.js eventShouldLiveIn``shouldLiveInRoom:false`), retroactively on reload — MUST ship the "N replies" summary chip in the same release. Roots stay in both timelines. |
| State | `roomIdToActiveThreadIdAtomFamily` (per-room, mirrors `roomIdToReplyDraftAtomFamily`) in new `state/room/thread.ts` + `getThreadDraftKey(roomId, threadRootId)` = `` `${roomId}::${threadRootId}` `` |
@@ -519,10 +527,12 @@ Exhaustive, low-level implementation details for backlog items. Follow these pat
| Mobile | Pure CSS like `MembersDrawer.css.ts`: fixed width toRem(360) desktop, `position:fixed; inset:0` under 750px. |
**Critical side-effect fixes (one-liners, land FIRST):**
1. `initMatrix.ts` → `threadSupport: true`.
2. `utils/notifications.ts:24` → `sendReadReceipt(latestEvent, type, /*unthreaded*/ true)` — otherwise markAsRead becomes `main`-scoped and room badges stick permanently unread (room unread total includes thread counts).
**Known SDK traps (verified):**
- **Local echo gap:** chronological pending ordering means the thread timelineSet never receives pending events (`canContain` rejects; `room.getPendingEvents()` THROWS in this mode) — ThreadTimeline must render its own pending strip via `RoomEvent.LocalEchoUpdated` filtering on `threadRootId`, deduped against `thread.findEventById`.
- **Bootstrap:** `room.getThread(id) ?? room.createThread(id, room.findEventById(id), [], false)` — the SDK auto-fetches via `/relations` and inserts the root at top; gate rendering on `thread.initialEventsFetched`; decrypt with `decryptAllTimelineEvent` after init + each pagination.
- **Deep links:** `getEventTimeline(mainSet, threadEventId)` returns undefined for thread events — redirect jump-to-event to the panel (best-effort v1).
+4 -4
View File
@@ -5,11 +5,11 @@ experimental_features:
msc3861:
enabled: true
issuer: http://localhost:8090/
client_id: "0000000000000000000SYNAPSE"
client_id: '0000000000000000000SYNAPSE'
client_auth_method: client_secret_basic
client_secret: "REPLACE_WITH_A_SHARED_CLIENT_SECRET"
admin_token: "REPLACE_WITH_A_LONG_SHARED_ADMIN_TOKEN"
account_management_url: "http://localhost:8090/account"
client_secret: 'REPLACE_WITH_A_SHARED_CLIENT_SECRET'
admin_token: 'REPLACE_WITH_A_LONG_SHARED_ADMIN_TOKEN'
account_management_url: 'http://localhost:8090/account'
# With msc3861 enabled, Synapse disables its own password/SSO login and advertises
# `m.authentication` in /.well-known/matrix/client — which is exactly what the