docs: add DP1-DP18 test items (LOTUS_TESTING §R), retire DP backlog
The discovery-pass items DP1-DP18 are implemented + TPVR-reviewed, so move their manual QA steps into LOTUS_TESTING.md (new section R) and replace the DP backlog block in LOTUS_TODO.md with a done-breadcrumb pointing at §R + the commits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -699,6 +699,40 @@ Paste each of these into a room and confirm a media tile (not a plain link) with
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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
|
## 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.
|
1. **O1 + O2** (threads + per-thread notifications) — the largest new surface; the main-timeline change is user-visible.
|
||||||
|
|||||||
+2
-121
@@ -63,128 +63,9 @@ Built and gate-green; verify per [LOTUS_TESTING.md](./LOTUS_TESTING.md), then gr
|
|||||||
|
|
||||||
## 🔴 Open — Actionable
|
## 🔴 Open — Actionable
|
||||||
|
|
||||||
### 🔍 Discovery pass (2026-07) — agent-surveyed, TPVR-verified
|
### ✅ Discovery pass (2026-07) — DP1–DP18 DONE
|
||||||
|
|
||||||
Three discovery agents swept the fork's own code (`src/`) for correctness / a11y /
|
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`.
|
||||||
tech-debt items beyond the tracked backlog; every item was then independently
|
|
||||||
re-verified (TPVR, 2026-07) against the code + installed SDK. **All 18 are real** —
|
|
||||||
`✅` = confirmed as claimed, `◐` = real but the claim was imprecise (correction inline).
|
|
||||||
`Sev` = user/maintenance impact, `Eff` = S/M/L. IDs `DP1–DP18`.
|
|
||||||
|
|
||||||
**Correctness (user-facing):**
|
|
||||||
|
|
||||||
- [ ] **DP1 — Silent slash-command failures (Sev High · Eff S–M) · ✅.** `RoomInput.tsx:542`
|
|
||||||
calls `commandContent.exe(plainText)` (all `exe` are async, several reject) with no
|
|
||||||
`await`/`.catch`, then unconditionally resets the editor as if it succeeded; no global
|
|
||||||
`unhandledrejection` handler exists. `/kick` `/ban` `/invite` `/join` `/leave` `/ignore`
|
|
||||||
`/myroomnick` `/acl` `/delete` etc. fail with **zero** feedback. **TPVR context:** `/kick`
|
|
||||||
& `/ban` also route through `rateLimitedActions` (`utils/matrix.ts:452`) whose `to()`
|
|
||||||
swallows all non-429 errors — so a real fix must surface errors **per-command**, not just
|
|
||||||
await line 542.
|
|
||||||
- [ ] **DP2 — Pending invites re-notify on every reload (Sev Med · Eff S) · ✅.**
|
|
||||||
`ClientNonUIFeatures.tsx:202-217` + `usePreviousValue(invites.length, 0)` (ref inits `0`).
|
|
||||||
**TPVR context:** on warm reload the invite atom is populated synchronously from cached
|
|
||||||
`mx.getRooms()` while the client is already `SYNCING`, so the sync-state gate (`:203`)
|
|
||||||
doesn't protect → `notify(N)` + sound fire for pre-existing invites. Fix: seed previous to
|
|
||||||
`invites.length` and/or a first-render ref.
|
|
||||||
- [ ] **DP3 — Status cleared on another device is resurrected (Sev Med · Eff S) · ✅.**
|
|
||||||
`Profile.tsx:370` gates on `presence?.status` truthy, so a remote **clear** (falsy) skips
|
|
||||||
both the input reset and the `localStorage` write; `usePresenceUpdater.readStatus()`
|
|
||||||
re-sends the stale `status_msg` on the next heartbeat. Fix: treat `status !== undefined`
|
|
||||||
as an explicit clear + `localStorage.removeItem(STATUS_MSG_KEY)`.
|
|
||||||
- [ ] **DP4 — Favourite / Low-priority toggles fail silently (Sev Low–Med · Eff S) · ✅.**
|
|
||||||
`RoomNavItem.tsx:337-351` — `setRoomTag`/`deleteRoomTag` fire-and-forget with no optimistic
|
|
||||||
UI (state derives from the account-data echo), so a rejected write leaves the menu closed,
|
|
||||||
no change, no toast. UX-only, bites on failure.
|
|
||||||
- [ ] **DP5 — Soundboard pack hooks go stale on room/space change (Sev Low · Eff S) · ✅.**
|
|
||||||
`useSoundboardPacks.ts:80,103,125` — `useState(() => …)` inits once. **TPVR context:** both
|
|
||||||
consumers usually remount when the room changes so the stale window is narrow, but it's a
|
|
||||||
real footgun. Fix: derive via `useMemo`/resync effect keyed on the room list.
|
|
||||||
- [ ] **DP6 — Call-decline event fire-and-forget (Sev Low · Eff S) · ✅.**
|
|
||||||
`CallEmbedProvider.tsx:547` — uncaught `mx.sendEvent(RTCDecline)`; UI dismisses
|
|
||||||
unconditionally (`:553`), so a failed decline looks handled locally while the caller keeps
|
|
||||||
ringing. Content shape verified spec-correct. Fix: best-effort `.catch` + still dismiss.
|
|
||||||
|
|
||||||
**a11y / UX / mobile:**
|
|
||||||
|
|
||||||
- [ ] **DP7 — Deafen button announces the OPPOSITE action to screen readers (Sev High · Eff S) · ✅.**
|
|
||||||
`call-status/CallControl.tsx:70` — `aria-label={enabled ? 'Undeafen' : 'Deafen'}` is
|
|
||||||
inverted; the tooltip (`:58`) is the **correct** one. When sound is ON, pressing deafens →
|
|
||||||
label should say "Deafen". aria-only fix, no visual risk.
|
|
||||||
- [ ] **DP8 — Sound/Video/ScreenShare toggles missing `aria-pressed` (Sev Med · Eff S) · ✅.**
|
|
||||||
Only `MicrophoneButton` (`CallControl.tsx:38`) exposes it; `:52,90,131` omit. **TPVR note:**
|
|
||||||
also pick a consistent pressed-semantics across all four (mic's `aria-pressed={!enabled}`
|
|
||||||
= "pressed means muted" is debatable).
|
|
||||||
- [ ] **DP9 — GifPicker drops focus + fixed width overflow (Sev Med · Eff S/M) · ✅ focus / ◐ width.**
|
|
||||||
`GifPicker.tsx:106` `returnFocusOnDeactivate:false` (+ `initialFocus:false`) drops focus to
|
|
||||||
`<body>` on dismiss — confirmed. **Correction:** `PICKER_WIDTH=312` is ~314px total and only
|
|
||||||
overflows *below* ~314 CSS-px or when PopOut positions near a screen edge (not a flat
|
|
||||||
"≤320px"). Fix: return focus + `min(312px, calc(100vw - 16px))`.
|
|
||||||
- [ ] **DP10 — Search-filter "clear" icons: redundant mouse-only affordance (Sev Low–Med · Eff S) · ◐.**
|
|
||||||
`message-search/SearchFilters.tsx:800,826,850` (+ `DateRangeButton:648`) — `<Icon onClick>`
|
|
||||||
inside a `Chip`(`<button>`). **Correction:** the `<Icon>` is a bare `<svg>` (no interactive
|
|
||||||
ARIA role) so it's **not** an invalid nested control, and the clear is **not**
|
|
||||||
keyboard-inaccessible — the parent Chip's own `onClick` (and DateRangeButton's menu "Clear")
|
|
||||||
performs the identical toggle. Real defect = a **redundant mouse-only** clear icon. Fix: drop
|
|
||||||
the icon onClick, or make it a focusable `IconButton`.
|
|
||||||
- [ ] **DP11 — Voice-recorder can overflow composer + no announced duration (Sev Med · Eff M) · ◐.**
|
|
||||||
`VoiceMessageRecorder.tsx:33,190-269`. **Correction:** the waveform is ~236px (not ~360); the
|
|
||||||
whole widget ~380px. Outer `Box`(`:192`) has no width cap + waveform `flexShrink:0` → can
|
|
||||||
overflow a ~360px composer. **aria-live caveat:** duration ticks every 100ms, so a naive
|
|
||||||
`aria-live="polite"` would spam a screen reader — expose a snapshot/less-frequent value. Fix:
|
|
||||||
cap/allow-shrink outer width + responsive bar count (verify on a real ~360px device).
|
|
||||||
- [ ] **DP12 — Live-call participant count not announced (Sev Low · Eff S) · ✅.**
|
|
||||||
`call-status/LiveChip.tsx:131` / `CallStatus.tsx:45` — the "{count} Live" chip updates
|
|
||||||
silently. Fix: a separate visually-hidden `role="status"` mirroring the count (cleaner than
|
|
||||||
making the button's own label live).
|
|
||||||
|
|
||||||
**Tech debt / maintainability:**
|
|
||||||
|
|
||||||
- [ ] **DP13 — Triplicated account-data list engine (Sev High · Eff M) · ✅.**
|
|
||||||
`hooks/useBookmarks.ts`(144) / `useReminders.ts`(132) / `useUserNotes.ts`(122) are
|
|
||||||
near-verbatim copies of a concurrency-critical engine (module `moduleState`,
|
|
||||||
`ensureModuleState`, `enqueue*Write` serialization to avoid `setAccountData` lost-update
|
|
||||||
clobbering, `listeners` set, per-client teardown). **TPVR context:** factory is clean — the
|
|
||||||
only real diff is the account-data key + payload shape (list `{items:[]}` vs notes' flat
|
|
||||||
`Record`), so parameterize container-vs-record. Fix: extract
|
|
||||||
`createAccountDataListStore<T>(key, extract, serialize)`; hooks become thin wrappers.
|
|
||||||
**Directly unblocks the custom-room-tags feature.**
|
|
||||||
- [ ] **DP14 — TDS hardcoded colors → wrong colors in TDS *light* mode (Sev High-small · Eff S) · ✅.**
|
|
||||||
`room/message/Message.tsx:107,111,115,131` (`#FF3B3B`, `rgba(0,212,255,…)`) and
|
|
||||||
`read-receipt-avatars/ReadReceiptAvatars.tsx:109,112,114` hardcode dark-theme accent
|
|
||||||
hex/rgba gated on `lotusTerminal`. TDS **overrides** `--lt-accent-cyan` → `#0062b8` and
|
|
||||||
`--lt-accent-red` → `#b5001f` in the light theme (`lotus-terminal.css.ts:410,418`), so these
|
|
||||||
stay bright cyan/red instead of the light theme's darker blue/red — a **real wrong-color
|
|
||||||
bug**, direct TDS-Design-Law violation. `EventReaders.tsx:75,86` is the correct
|
|
||||||
`var(--lt-accent-*)` / `var(--lt-box-glow-*)` template.
|
|
||||||
- [ ] **DP15 — `sendStateEvent(…, StateEvent.X as any, …)` cluster (Sev Med · Eff M) · ◐ (count).**
|
|
||||||
**Correction:** real count is **32** casts across 20 files (not 28); `as any` on arg 2 also
|
|
||||||
collapses content typing to `any`. Plus 2 *dynamic* casts in dev tools (`SendRoomEvent.tsx:54`,
|
|
||||||
`StateEventEditor.tsx:62`) that an enum-typed helper can't remove. Fix: one typed
|
|
||||||
`sendState(mx, roomId, type: StateEvent, content, stateKey?)` helper eliminates the 32.
|
|
||||||
- [ ] **DP16 — `(mx as any).get/setAccountData` cluster (Sev Med · Eff S–M) · ✅.**
|
|
||||||
**TPVR context:** real count **19** casts across 13 files; the existing `utils/room.ts:45
|
|
||||||
getAccountData` helper itself casts internally and is bypassed by most callers, and there's
|
|
||||||
**no** `setAccountData` helper. Fix: typed `getAccountData<T>` / `setAccountData<T>` wrappers;
|
|
||||||
route all callers (incl. the DP13 hooks) through them.
|
|
||||||
- [ ] **DP17 — `UrlPreviewCard.tsx` literal-emoji icons + hardcoded brand hex (Sev Med · Eff S / L) · ✅.**
|
|
||||||
Exactly **2250 lines**. Literal glyphs `♫`(`:477,520,1476`) `▶`(`:515`) `🎮`(`:862`)
|
|
||||||
`💬`(`:1008`) `⚙`(`:1536`) where folds `Icon`/`Icons` is mandated; inline brand hex `#EE1D52`
|
|
||||||
(TikTok) `#1db954` (Spotify) `#c7d5e0` (Steam). Fix (S): glyphs → folds Icons + hoist brand
|
|
||||||
consts. Optional (L): split the per-provider render branches.
|
|
||||||
- [ ] **DP18 — Member-avatar render trio duplicated (Sev Low–Med · Eff M) · ◐ (count).**
|
|
||||||
**Correction:** the full trio (`getMemberDisplayName(room,id) ?? getMxIdLocalPart(id) ?? id`
|
|
||||||
+ `getMember(id)?.getMxcAvatarUrl()` + `mxcUrlToHttp(…)`) is in **8** components
|
|
||||||
(`EventReaders`, `UserMentionAutocomplete`, `ReadReceiptAvatars`, `MemberTile`,
|
|
||||||
`ClientNonUIFeatures`, `SearchInput`, `ReactionViewer`, `MembersDrawer`) — not 9
|
|
||||||
(`ForwardMessageDialog` uses a *room* avatar). Separately the name-fallback subpattern alone
|
|
||||||
(`getMemberDisplayName ?? getMxIdLocalPart`) is in ~29 sites / 24 files. Fix: two extractions
|
|
||||||
— `useMemberAvatar(room, userId)` (8 sites, also standardizes the DP-N6 reactivity story) +
|
|
||||||
`getMemberName(room, userId)` (24 sites).
|
|
||||||
|
|
||||||
_Minor cleared non-issues (checked, NOT bugs): commented-out `console.log` in `ASCIILexicalTable.ts`;
|
|
||||||
grep-flagged `addEventListener` "leaks" that are transient `Audio` elements; `useModalStyle` already
|
|
||||||
gives mobile full-screen dialogs app-wide._
|
|
||||||
|
|
||||||
### ✅ Unread/read-receipt flakiness (reported 2026-07) — FIXED (pending prod QA)
|
### ✅ Unread/read-receipt flakiness (reported 2026-07) — FIXED (pending prod QA)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user