diff --git a/LOTUS_TODO.md b/LOTUS_TODO.md index 8f56c2300..4bbb35f7e 100644 --- a/LOTUS_TODO.md +++ b/LOTUS_TODO.md @@ -33,6 +33,41 @@ Completed features are documented in [LOTUS_FEATURES.md](./LOTUS_FEATURES.md). --- +## πŸ”Ž Audit findings β€” Wave 1 (2026-07) + +Bug-hunt of the Tier-1 high-risk areas (notifications/unread/receipts, threads, calls host-side, Element Call fork) by 4 parallel deep-audit agents. **Findings only β€” not yet fixed.** Fix in prioritized, individually-tested passes (each πŸ”΄ first). `[T#]`=threads, `[N#]`=notifications, `[C#]`=calls host, `[EC#]`=fork. Top-two πŸ”΄ hand-verified against the code. + +### πŸ”΄ High β€” data-integrity / broken core UX + +| ID | Defect | File:line | Repro | Fix sketch | +| :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | +| **T1** | **`markThreadAsRead` corrupts the room's MAIN read marker** β€” sends `sendReadReceipt(latestEvent, Read)` with `unthreaded` omitted (β†’false); `latestEvent` is the thread **root** when replies aren't loaded, so the SDK writes a `thread_id:"main"` receipt at the old root β†’ `getEventReadUpTo` jumps back β†’ room re-lights. **Same class as the P6 regression, second path.** CONFIRMED. | `features/room/thread/useThread.ts:172` (fired on mount by `ThreadPanel.tsx:145-167`) | Open a thread whose root is an older message (or any thread before replies load) β†’ previously-read main messages resurface as unread; room dot won't clear. **Likely a live cause of the current "unread won't clear."** | Mirror `notifications.ts`: use `thread.lastReply()` and bail if null, or `if (latestEvent.getId() === thread.id) return;` β€” only receipt a real reply. | +| **N1** | Favicon + tab-title unread count **double-counts space aggregates** β€” `putUnreadInfo` writes an entry for the leaf room AND every ancestor space; `FaviconUpdater` sums **all** map entries. CONFIRMED. | `pages/client/ClientNonUIFeatures.tsx:97-103` (FaviconUpdater) vs `state/room/roomToUnread.ts:67-74` | A room w/ 3 highlights nested in Spaceβ†’Subspace β†’ tab title reads `(9)` not `(3)`. Orphan-only users unaffected (why it hid). | Sum only leaf entries (skip aggregates: leaves have `from:null`), or compute from `mx.getRooms()`. | +| **N2** | **Missed notifications/sounds** β€” the `deliverNotification` dedupe cache (`unreadCacheRef`) is written only on incoming messages, never cleared on read. `unreadInfo.total` resets to 0 on read but the cache keeps the stale value. CONFIRMED. | `pages/client/ClientNonUIFeatures.tsx:364-408` | DM cadence "msgβ†’readβ†’msgβ†’read…": after the first, every later message's notification+sound is suppressed (`unreadEqual(1,1)`). | Clear/refresh `unreadCacheRef` from a `RoomEvent.Receipt` listener, or dedupe on last-notified `eventId` (as the thread path already does). | + +### 🟠 Medium β€” visible wrong behavior + +| ID | Defect | File:line | Notes | +| :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | +| **T4** | Explicit per-thread **"All" override is defeated** by the room-count gate: `deliverNotification` returns early on `unreadInfo.total===0` and dedupes on `unreadEqual`, but a non-mention thread reply in a Mentions-only room doesn't bump the count β†’ "notify all replies" silently drops. CONFIRMED. | `ClientNonUIFeatures.tsx:370-380` vs `:489-502` | Don't gate the thread path on room `total`; dedupe on thread event id (`lastNotifiedThreadRef`) only. | +| **N3/T3+** | **Muted-thread-only room gets a phantom unread dot** β€” `getUnreadInfos` pushes a `{0,0}` entry (roomHaveNotification true from the muted thread's server count), which still lights the nav row + pollutes "unread only" filters. CONFIRMED. | `utils/room.ts:261-278` | Don't push when `getUnreadInfo`β†’`{0,0}` and `roomHaveUnread` is false. | +| **N4/T3** | Reading a thread **DELETEs the whole room badge** β€” `handleReceipt` unconditionally `DELETE`s on any of my receipts; if the thread was already read (no `UnreadNotifications` PUT follows) the room's legit main-timeline badge vanishes until the next event. Also a PUT/DELETE ordering race. PLAUSIBLE. | `state/room/roomToUnread.ts:243-263` | For threaded receipts, recompute via `getUnreadInfo` PUT instead of blanket DELETE. | +| **T2** | Thread **"Jump to Latest" doesn't reset the virtual window** β€” only bumps scroll count; when scrolled up with live replies arriving, newest replies aren't rendered so the chip lands on a mid/old event. CONFIRMED. | `features/room/thread/ThreadTimeline.tsx:462-468` | Re-anchor with `setTimeline(getInitialThreadTimeline(...))` like the main timeline's `handleJumpToLatest`. | +| **C-H1** | `forceState()` **re-runs on every `JoinCall`** (no once-guard) β†’ on an EC reconnect it resets live mic/video/deafen back to the join-time snapshot, clobbering the user's mid-call toggles. PLAUSIBLE (structurally confirmed). | `plugins/call/CallEmbed.ts:362-364,502-509` + `CallControl.ts:141-156` | Guard `onCallJoined` to `forceState` only on first join, or re-apply _current_ state on rejoin. | +| **C-H2** | AFK auto-mute monitors the **browser default mic**, not EC's selected device β†’ measures silence while the user talks on a non-default mic β†’ **auto-mutes an active speaker**; also a 2nd OS mic indicator. PLAUSIBLE. | `hooks/useAfkAutoMute.ts:39-40` | Acquire with EC's selected `deviceId`, or tap EC's existing track. | +| **C-H3** | Screenshare/spotlight **state observer goes stale** β€” `bodyMutationObserver` uses `subtree:false`, so when EC re-renders its controls subtree `observeControls()` never re-runs and the reflected screenshare/spotlight boolean freezes. PLAUSIBLE. | `plugins/call/CallControl.ts:168-200,274-287` | Observe the stable controls container with `subtree:true`, or re-query on each mutation. | + +### 🟑 Low / minor / cosmetic / perf + +- **Calls host:** C-M1 deafen DOM-fallback leaks late-added `