Files
cinny/LOTUS_TESTING.md
T
jared 8192da5a12
CI / Build & Quality Checks (push) Successful in 10m41s
CI / Trigger Desktop Build (push) Successful in 29s
fix(notifications): clear thread receipts on mark-read; cap avatar-decoration refetch
Two federated-room bugs surfaced by the desktop build:

1. markAsRead only sent one unthreaded receipt at the main-timeline tail. With
   threadSupport enabled, thread replies leave the main timeline, so a reply
   newer than that tail was never covered — its per-thread notification count
   (which the room dot sums) lingered, so the unread dot never cleared even
   after reading. It also early-returned when the main timeline was already
   read. Now also send a threaded receipt at each unread thread's latest reply.

2. useAvatarDecoration never cached non-404 failures, so every avatar mount
   re-requested io.lotus.avatar_decoration for federated users whose homeserver
   403s/502s the field — a refetch storm that spammed the console and hammered
   our homeserver's federation. Now cache definitive rejections (400/403/404)
   and give up after ~2 transient (429/5xx) attempts per session.

Gates: tsc/eslint/prettier clean, build OK, 665 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:31:10 -04:00

57 KiB
Raw Blame History

Lotus Chat — Manual Testing Guide

Generated: June 2026 · Updated: July 2026 (added §O — threads, per-thread notifications, math, search cache, session hardening, audit wave, desktop CSP) Scope: Everything landed on the lotus branch since the v4.12.3 merge that I (Claude) could not verify statically and that needs a human in a real environment to confirm. Work through it top-to-bottom; the highest-risk / hardest-to-reproduce items are first.

How to report back: For each numbered check, tell me PASS / FAIL (or partial). On any FAIL, include: what you saw vs. expected, the browser/OS (and whether web LXC 106 or the desktop/Tauri build), the theme you were on, and any browser console errors (F12 → Console). Screenshots help for anything visual.

Environment notes

  • You push from your own machine; these commits are local on lotus until you do.
  • Test the web build (LXC 106 / code.lotusguild.org) first; re-run the call + poll sections on the desktop (Tauri) build too, since CSP and the EC iframe behave differently there.
  • Several call features need a second participant (second account on another device/browser, or a colleague). Items that need this are marked 👥 2 people.
  • A couple of call items need a third room/call in parallel — marked 👥👥.

Commits covered

Commit Area
caf6318a Poll vote buttons → folds tokens (N4)
c67aed01 In-call incoming-call banner (#4b)
4a875884 Selectable ringtone (#4a)
0394fce9 EC iframe load watchdog + recovery UI; avatar decorations on call tiles (#3)
d2946c00 Upload retry/backoff, presence-on-unload, typed m.direct
b7e1f89c Timeline/composer/emoji perf memoization
c0f98672 Upstream Element Call 0.20.1 merge (regression sweep)

A. Calls — new ringtone + notification work (highest priority)

A1. Ringtone selection — preview in Settings

Steps

  1. Open Settings → General, scroll to the Calls section.
  2. Find the new Ringtone dropdown (just above Ringtone Volume).
  3. Select each option in turn: Classic, Chime, Soft, Retro, Silent.

Expected

  • Selecting Classic plays the existing call.ogg clip (cut off after a few seconds).
  • Chime / Soft / Retro each play a short, distinct synthesized preview.
  • Silent plays nothing.
  • Changing Ringtone Volume then re-selecting a ringtone previews at the new volume.
  • No console errors.

⚠️ Known browser limitation: the synthesized tones use WebAudio. If a preview is ever silent, click anywhere on the page once (a "user gesture") and retry — browsers suspend audio until the page has been interacted with. The Settings preview is after a click so it should always sound; this note matters more for A3.

A2. Ringtone selection persists

  1. Set Ringtone to Retro, reload the app.
  2. Expected: the dropdown still shows Retro (setting persisted).
  3. Bonus: in devtools, set localStorage.settings to a bogus ringtoneId and reload → it should fall back to Classic, not break.

A3. Incoming call uses the selected ringtone — 👥 2 people

Setup: Account A (you) and Account B in a DM or a private (invite-only) group room.

  1. As A, pick a non-silent ringtone (e.g. Chime).
  2. From B, start a call in that DM/room. Do not answer on A.

Expected on A

  • The full-screen Incoming Call dialog appears (caller name, room avatar, Answer / Reject).
  • The selected ringtone loops until you answer/reject/ignore (at the set volume).
  • Answer → joins the call. Reject (DM) / Ignore (group) → dialog dismisses and ring stops.
  • Set ringtone to Silent and repeat → dialog still appears, no sound.

A4. In-call banner for a second incoming call — 👥👥 (the trickiest one)

Setup: You (A) already in a call in Room 1. Account B can call you in a different Room 2 (a DM or private group you share). Ideally a third account C, or B leaves Room 1's call first.

  1. While A is actively in Room 1's call, trigger an incoming call to A from Room 2.

Expected on A

  • No full-screen takeover. Instead a compact banner appears in the top-right corner with the caller's avatar, room name, "Incoming voice/video call", and Answer / Reject (or Ignore) buttons.
  • It plays a single soft ping, not a looping ring (so it doesn't talk over your active call).
  • The banner does not cover your active call's controls/PiP in a way that blocks them.
  • Answer → switches you into Room 2's call. Reject/Ignore → banner disappears.
  • The banner auto-dismisses if the caller hangs up / the call times out.

Also verify the no-op case: while in Room 1's call, if a notification for Room 1 itself arrives, nothing should pop up (no banner, no dialog).

A5. Camera focus during screenshare (#1) — 👥 2 people

Setup: You (A) and B in a call; B (or another participant) sharing their screen, and at least one person with camera on.

  1. As A, open the participant glance (the stacked avatars / member list for the call) and click a participant who has their camera on.
  2. In the menu, click "Focus camera".

Expected

  • The view switches to spotlight and pins that person's camera tile, overriding the auto-spotlighted screenshare.
  • It stays on that camera (doesn't immediately snap back to the screenshare).
  • If you pick someone with their camera off, it should at worst just toggle spotlight (graceful fallback), not error.

A6. Avatar decorations on call tiles (#3) — 👥 2 people

Setup: A participant in the call has an avatar decoration set (Settings → Profile decoration).

  1. Join a call with that participant.
  2. Look at our participant roster / prescreen tiles (not the avatars rendered inside the Element Call video grid — those are EC's and out of scope).

Expected: the decoration ring/overlay renders around that participant's avatar on the call tile, the same way it does in member lists.

A7. EC iframe load watchdog + recovery UI (#EC, N96)

This guards against a permanently-stuck "Loading…" call. Also covers the N96 button-label fix (the old "Retry" and "Leave" buttons were identical — now there is a single "Back" button).

  1. Normal case: join a call → it should connect within a few seconds as usual (the watchdog stays invisible).
  2. Failure case (best-effort to reproduce): throttle your network hard (devtools → Network → Offline) right as you click join, or block the Element Call origin, so the iframe can't finish loading.

Expected

  • On a genuine failure/timeout (~25s), instead of an endless spinner you get a visible error overlay with a single "Back" button (the old "Retry" + "Leave" pair is gone — they did the same thing and "Retry" was misleading).
  • Clicking Back returns you to the call prescreen, where you can manually click Join to try again.
  • Normal joins must not trigger the error overlay (no false positives) — this is the important part to confirm.
  • Self-heal: if the error overlay appears on a slow network but EC then finishes loading anyway, the overlay should dismiss itself and drop you into the live call. Worth confirming on a deliberately throttled-but-not-blocked connection.

B. Polls (N4) — render correctly on non-TDS themes

This was the actual bug: poll buttons used undefined CSS variables, so on the default (non-Lotus-Terminal) themes they rendered with invisible borders / no selected state.

B1. Poll renders on a default theme — PASS

  1. Switch to a default Cinny theme (Settings → Appearance — not Lotus Terminal / TDS). Test both a dark and a light theme.
  2. In any room, create a poll (composer → poll button): a single-choice poll with 3 options.

Expected

  • Each option is a clearly bordered button with visible rounded corners.
  • A radio circle indicator is visible on the left of each option.
  • Text, and (after votes) the percentage, are legible.

B2. Voting + selected/progress state

  1. Vote on an option. Expected
  • The selected option shows a filled accent border + filled radio, and an accent progress-bar fill grows behind it proportional to the vote %.
  • The percentage and total vote count update.
  • Click again / pick another option → selection moves correctly (single-choice replaces; the bar redraws).

B3. Multiple-choice poll

  1. Create a poll allowing multiple selections. Expected
  • Indicators are square checkboxes (not circles); selected ones show a that's legible against the filled box.
  • You can select several options; each shows its own progress fill.

B4. Lotus Terminal theme regression — PASS

  1. Switch to Lotus Terminal / TDS theme and re-open a poll. Expected: still looks correct (the fix uses theme tokens, so the TDS accent should now drive it) — no worse than before.

C. Robustness / background behavior

C1. Presence updates on tab close

  1. Open the app, then close the tab (or quit the browser).
  2. From another session/device, check your presence shortly after. Expected: you go offline/away reliably (the unload now uses fetch({keepalive})). Previously this could be missed.

C2. Upload retry on flaky network (best-effort)

  1. In devtools → Network, set a throttle that drops/slows requests, or toggle Offline briefly during a file upload. Expected
  • A transient failure retries (up to 3×, with backoff) and the upload can still succeed once the network recovers.
  • A genuine, permanent rejection (e.g. file too large / 4xx) still fails fast with the usual error — it should not spin retrying.

C3. General timeline/composer perf (no functional regression)

The memoization changes are invisible if correct. Just confirm nothing broke:

  • Open a busy room; scrolling, jump-to-latest, mark-as-read all still work.
  • Composer: send a message, upload a file, share a location, pick an emoji and a sticker — all still work.

D. Element Call 0.20.1 merge — regression sweep (👥 2 people)

The upstream bump changed EC's internals and DOM selectors; our call controls drive that iframe, so sweep them. In a live call with 2 people, confirm each of our control-bar buttons works:

  • Mic mute/unmute (icon + actual audio)
  • Camera on/off
  • Deafen / Sound toggle (your deafen key too)
  • Screenshare start/stop (and the "Share your screen?" confirm)
  • Screenshare audio mute toggle
  • Fullscreen toggle
  • ⋮ More menu → Spotlight/Grid, Reactions, Settings each open the right EC panel
  • End call leaves cleanly
  • PTT (push-to-talk) if enabled: hold key = transmit, release = mute; releasing on blur works
  • AFK auto-mute if enabled: goes muted after the timeout
  • PiP (picture-in-picture) mini window: drag, resize, fullscreen button, return-to-call; the "You muted" / "All muted" badges show on the right person
  • Denoise (if ML noise suppression enabled): call audio still flows, no silence

If any control does nothing, that usually means an EC DOM selector changed — capture the console and tell me which button.


D2. Element Call fork — Phase 2 feature sweep (👥 2 people) — 0.20.1-lotus.1

The whole EC iframe is now our self-built fork (@lotusguild/element-call-embedded@0.20.1-lotus.1). Five features are active (the host sets their flags / sends their actions); two ship dormant. Confirm you're on the fork first: EC iframe console prints Element Call embedded-v0.20.1-lotus.1 (the old build prints embedded-v0.20.1). If it says the old version, the web deploy hasn't landed — the fork features won't be present, so don't test D2 yet. For non-dev testers, each item below also states the plain " good if / tell us if" outcome.

D2-1. Denoise in-source — survives reconnect (fixes A7) highest risk (everyone's mic)

Flag: cinny sets lotusDenoiseSource=1 when ML denoise is selected (the old build-time getUserMedia shim is removed). This is the single change with the widest blast radius — test deliberately.

  • Audio flows, no silence with ML denoise on (baseline, also §D line 204).
  • Reconnect (the A7 fix): in a call with ML denoise on, kill network ~10 s (devtools → Offline) so EC shows "Connection lost / Reconnect", then restore. Mic still works AND still denoised afterward, without End+rejoin. (This is the exact bug that was reintroduced then fixed; if it regresses, mic dies on every reconnect.)
  • Mic device switch mid-call (Settings → change microphone): audio keeps working (same restart() path as reconnect).
  • Mute → unmute a few times: audio returns each time.
  • Each model if the picker offers them: rnnoise (default), speex, dtln, deepfilternet — each loads + denoises, no silence. (All four are in-source now; DTLN runs at 16 kHz, others 48 kHz.)
  • No double-processing: audio isn't over-suppressed/artifacted (would mean the old shim is still injected alongside the in-source engine).
  • Rollback if bad for everyone: revert the cinny deploy commit (restores the shim + @element-hq parity).

D2-2. Speaking + mute indicators from widget events (#2)

Flag: lotusCallState=1. cinny now reads speaker/mute state from io.lotus.call_state events instead of scraping EC's DOM (DOM fallback retained). Overlaps G1.

  • Speaking glow lights the correct person when they talk (you, then your friend).
  • PiP "All muted" / "You muted" badge points at the right person and updates on mute/unmute.

D2-3. Focus camera during a screenshare (#4 / A5)

Action: cinny sends io.lotus.focus_participant (the DOM .click() hack is gone). Overlaps A5 / G2.

  • Person A screenshares; Person B camera on; MemberGlance → Focus camera on B → B's camera is spotlighted alongside/over the shared screen (not ignored).
  • Camera-off target = graceful (no error, no kick out of the screenshare).

D2-4. In-call avatar decorations (#6) — NEW, beyond A6

Action: cinny pushes io.lotus.decorations. A6 only covered the lobby roster and called in-call EC tiles out of scope — that's now in scope.

  • A participant with a Profile decoration joins camera off → the decoration ring renders on their in-call video-tile avatar (inside EC, not just the lobby), correctly sized/positioned.
  • Decoration tracks the right person across grid/spotlight layout changes; disappears when they leave.

D2-5. Native transparent background (#5)

Flag: lotusTransparent=1 (native, replacing the injected background:none !important).

  • Call background looks right — host wallpaper/surface shows through; no black box, bad see-through, or layout breakage (also covered loosely by §D2 "looks right").

D2-7. In-Call Soundboard (#3 / P5-15) — 👥 2 people — NEW

Flag: lotusAudioInject=1. A 🔔 Soundboard button now sits in the call controls bar (left group, next to the chat button). Clips are user-uploadable and sync across your devices like emoji packs. Prereq: Settings → General → Calls → Soundboard must be ON (default on).

  • Upload: open the soundboard popout → Upload → pick a short audio file (mp3/ogg/wav, ≤ 1 MB). It appears as a clip tile. (Too-big / too-many shows an error, doesn't crash.)
  • Plays into the call: with a second person in the call, click a clip. They hear it, and you hear it locally too. good if both hear it; tell us if only one side does.
  • Sync: the uploaded clip shows up on your other device/session (account-data sync).
  • Delete: the ✕ on a tile removes it (everywhere, after sync).
  • Off switch: turn Settings → Calls → Soundboard off → the call-bar button disappears.
  • Injecting a clip does not mute/interrupt your mic or anyone else's audio.

D2-8. Call Quality Controls (#7 / P5-31) — 👥 2 people — NEW

Action: io.lotus.set_quality. User settings in Settings → General → Calls (Microphone Bitrate, Screenshare Bitrate, Screenshare Framerate; all default Auto). Admin caps in Room Settings → General → Voice → Call Quality Caps.

  • No regression at Auto: with everything on Auto, calls/screenshare work exactly as before.
  • User cap takes effect: set Microphone Bitrate to 32 kbps, rejoin/continue a call — audio still flows (thinner is fine). Set Screenshare Framerate to 15 fps and share your screen — it still shares. tell us if any setting kills audio/screenshare.
  • Applies mid-call: changing a setting during a call takes effect without End+rejoin.
  • Room-admin cap (admin needed): as a room admin, set Max Microphone Bitrate = 64 kbps in Room Settings → Voice. A member whose user setting is higher (e.g. 256) should be clamped to 64 (best-effort/UX — this is client-side; hard server enforcement is a separate follow-up).
  • Resetting a setting back to Auto removes the cap for the rest of the call.

Soundboard + quality are no longer "dormant" — if either does nothing, grab the EC iframe console and check for io.lotus.inject_audio / io.lotus.set_quality rejections.

D2-9. Call Permissions — HARD server-side, cross-client (👥 2 people, admin) — NEW

This is enforced by the voice-limit-guard on the server (re-signs the LiveKit JWT), so it applies to every client, not just Lotus Chat. Set in Room Settings → General → Voice → Call Permissions. (Requires the guard deployed on LXC 151 — auto-deploys on a matrix repo push.)

  • Disable screenshare: as admin, turn Allow Screen Sharing off. In a call, the screenshare button disappears in Lotus Chat. good if no one can screenshare.
  • Cross-client (the important one): have someone join the same room from stock Element / Element X and try to screenshare → the server refuses the track (it won't publish). This proves it's not just our client hiding a button.
  • Audio-only room: turn Allow Camera off too → the camera button disappears and cameras are server-blocked for all clients; microphones still work.
  • Live kill (mid-call): while someone is actively screensharing, an admin turns Allow Screen Sharing off. Within a few seconds their screenshare should stop for everyone on its own (no rejoin needed) — this is the server reconcile loop revoking it live. Works even if the sharer is on stock Element. good if the share drops within ~35 s; tell us if it keeps going.
  • Turning it back on restores the ability to screenshare/camera (start a new share).
  • No policy = no change: a room with Call Permissions left on defaults behaves exactly as before.

If any D2 item fails, grab the EC iframe console (right-click the call → inspect the iframe) — a widget-action/payload mismatch shows up there as a io.lotus.* rejection or a MissingKey/transport log.


Backlog of previously-fixed-but-unverified items

Sections AD above are this session's work. Everything below was fixed in earlier waves and is still flagged ⚠️ UNTESTED (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 AD are; do them as you have the right device handy.

E. Mobile / responsive (needs a real phone, or devtools device emulation)

E1. Composer toolbar touch targets (#7)

On a phone, open a room and the composer toolbar. Tap each button (attach, format, sticker, emoji, GIF, location, poll, schedule, send). Expected: every button is comfortably tappable (≥44×44px), no mis-taps hitting the wrong icon.

E2. Room Settings — no horizontal overflow (#8)

On a narrow phone screen, open Room Settings. Expected: the settings nav panel fills the full width; no horizontal scrollbar / sideways scrolling anywhere in the panel.

E3. Modals go fullscreen on mobile (#9)

On a phone, open several dialogs: Leave Room, Create Room, Create Space, Invite User, Report (room/user/message), Edit History, Forward Message, Remind Me, Schedule Message, Device Verification, Poll Creator. Expected: each opens fullscreen (no floating box, no rounded corners / max-width margins). On desktop the same modals should still be the normal centered boxes.

E4. Composer not hidden by the keyboard (#10) — iOS Safari especially

On a phone (priority: iOS Safari), tap into the composer so the on-screen keyboard appears. Expected: the composer input stays visible above the keyboard; the layout shrinks rather than the composer sliding under the keyboard.

E5. Mobile "Saved Messages" access (Mobile Bookmarks)

On a phone, inside a room, open the room header ··· More Options menu. Expected: a "Saved Messages" item is present; tapping it opens the bookmarks panel. (This was the only in-room access point missing on mobile.)


F. Visual / theming

F1. Animated chat background — no flicker (#2)

Settings → set an animated chat background (e.g. anim-rain / anim-aurora / anim-stars). Watch the message text and composer while it animates. Expected: smooth animation, no flickering / shimmering on message text or the composer, especially after scrolling. Note your GPU/browser if you see artifacts.

F2. Background vs. Seasonal theme are mutually exclusive (#6)

In Settings → Appearance:

  1. Pick a chat background → confirm any seasonal theme auto-switches off.
  2. Pick a seasonal theme → confirm the chat background auto-clears to none.
  3. (Edge) If you have old data with both set, after reload only one should visibly apply (no double-overlay clutter).

F3. Background / seasonal picker grid layout (N81)

In Settings → Appearance, look at the Chat Background and Seasonal Theme swatch grids; resize the window narrow→wide. Expected: swatches reflow to fill each row evenly (responsive grid), with no lopsided/orphaned last row at any width.


G. Calls — additional unverified (👥 2 people)

G1. PiP mute badges point at the right person (#12)

In a call with at least one other person, pop out the Picture-in-Picture mini window.

  • You mute your own mic → a "You"/muted badge appears bottom-left (your status).
  • A remote participant (or all of them) mutes → an "All muted" badge appears top-right (clearly about other people). Expected: the bottom-left badge is never triggered by someone else muting — that was the original bug (it looked like your own mic was muted when it wasn't).

G2. Full-screen camera broadcasts

  1. In a camera-only call (no screenshare), confirm the Fullscreen button is available (previously only showed during screenshare).
  2. Use MemberGlance → Focus camera to full-screen/spotlight a specific person's camera. (Overlaps A5; if you've done A5 you can skip.)

G3. PTT badge renders on all themes (N53)

Enable Push-to-talk (Settings → Calls) and join a call. Hold the PTT key. Expected: the floating PTT badge above the controls shows "PTT — Hold KEY" when idle and "● Live" (green) while held — on both a default theme and Lotus Terminal (it's now a single folds Chip; the old terminal-only variant was removed).


H. Media / performance (needs a room with many images)

H1. Lazy image decryption (P5-5 / MediaGallery)

Open a room / media gallery with many images (ideally encrypted). Scroll down through them. Expected: images decrypt/load as they approach the viewport, not all at once on open; scrolling stays smooth and memory doesn't balloon. Off-screen images shouldn't all decode up front.

H2. Thumbnail framing (P5-6)

Look at tall portrait images in the timeline and in the media gallery. Expected: thumbnails are framed center-top (so faces/subjects at the top aren't cropped out); no awkward stretching. Opening the full-size viewer still shows the whole image (contain, not cropped).


I. Accessibility (needs a screen reader: VoiceOver / NVDA / TalkBack)

With a screen reader on, navigate message hover-actions and content and confirm each control announces a meaningful label (not "button" / blank):

  • Reaction buttons announce the emoji + count (e.g. "thumbsup reaction, 3 people").
  • Edit history button announces "View edit history".
  • Thread indicator announces "View thread".
  • Reply (jump to original) announces "Jump to original message".

J. Desktop / Tauri build only

J1. Proactive update notifications (P5-40)

In the desktop (Tauri) build, with an update available, launch the app (and/or leave it running ~12h). Expected: an in-app toast/badge alerts you that an update is available, without manually checking Settings. (Needs an actual newer release to point at.)

J2. DTLN noise suppression sanity

In Settings → Calls, enable ML noise suppression with the DTLN model, then join a call. Expected: your mic audio still flows (no silence/robotic dropouts) and background noise is reduced. Confirmed working earlier but flagged for a final real-call check; verify on both web and desktop.


K. Features — end-to-end unverified

K1. Remind Me Later

On a message, ··· → Remind Me, pick a short preset (the 20-min one, or wait one out). Expected: when due, a Lotus toast fires linking to that message; the reminder then clears itself. Survives a reload while pending (stored in account data).

K2. Advanced search filters (P4-9)

In message search: use the sender picker (instead of typing from:@user), the date-range quick presets (Today / Last week / Last month / Last year), and the Has link toggle. Expected: each narrows results correctly and reflects in the search.

K3. Notification content + click target (P5-20 partial)

Trigger a desktop/browser notification for a new message. Expected: it shows the real message body (username: message, not "New inbox notification from…"); clicking it brings the window to front and navigates directly to that message (not just the inbox).


L. Fixed — verify

L1. AFK auto-mute releases the OS microphone indicator on mute (N95) — 👥 live call

Context (now FIXED): useAfkAutoMute.ts opened its own getUserMedia level-monitor capture for the whole call, so the OS recording indicator (green dot on macOS, mic icon on Windows/Linux) stayed lit even when muted. The capture is now gated on the reactive mic-on state — it runs only while unmuted, so muting releases the stream.

To verify:

  1. Enable AFK auto-mute in Settings → Calls and join a call.
  2. Manually mute your mic using the call controls → the OS recording indicator should clear within ~a second.
  3. Unmute → the indicator should re-appear (capture re-acquired).
  4. Also confirm AFK still works end-to-end: stay unmuted and silent past the configured timeout → mic auto-mutes with the "muted after inactivity" toast, and the indicator clears.

L2. Maskable PWA icon (N108) — Android install

  1. On Android Chrome, install Lotus Chat as a PWA (Add to Home Screen).
  2. Look at the home-screen icon.

Expected: the icon fills the adaptive-icon shape cleanly (the logo centered with safe-zone padding on the dark background), not clipped at the corners or floating in an odd box. Also worth a quick check in Chrome DevTools → Application → Manifest that the two purpose: maskable icons load without a 404 (this also validates the manifest's icon paths resolve in production — a pre-existing path convention I couldn't verify statically).


M. New features (this round)

M1. Search: has:image / has:file / has:video filters

  1. Open message search (in a room with shared images/files/videos in history).
  2. Run a broad search, then toggle the Images, Files, Video chips (in the filter bar, next to "Has link").

Expected:

  • Each chip narrows the visible results to that message type; multiple active chips = union (any of them).
  • Toggling them off restores the full results. The existing room/sender/date/has-link filters still work alongside.
  • Known limitation (by design): filtering is client-side over already-fetched results, so the visible count can be lower than the server's total for that query — paginating/loading more pulls in more to filter. Confirm this reads acceptably.

M2. Search: recent searches

  1. Run a few different searches, then clear the search box and focus it.

Expected: your last (up to 10) distinct searches appear as clickable chips; clicking one re-runs it. A Clear affordance wipes the list. The list persists across a page refresh (localStorage).

M3. Custom accent color (non-TDS themes) — ⚠️ needs your visual judgment

  1. Make sure Lotus Terminal (TDS) is off. Settings → Appearance → Custom Accent Color → pick a color.

Expected:

  • The app's accent (buttons, selected/active states, links, primary chips) recolors to your choice live.
  • Look critically at quality (this is the part I can't verify): button text legibility (OnMain contrast) on the accent buttons; hover/active shades; and selected-row / chip backgrounds (the translucent "Container" tints). Try a light color and a dark color and a saturated one.
    • If a dark accent makes selected-row text (OnContainer) hard to read, tell me — that's the one spot in the auto-derived palette most likely to need tuning.
  • Reset clears it back to the theme default.
  • Turn Lotus Terminal ON → the custom accent should be ignored (TDS fixed palette wins) and the picker shows a "non-TDS only" note; turn it back off → custom accent returns.
  • Reload → the chosen accent persists.

M4. Search: "Pinned only" filter

In message search, toggle the Pinned chip. Expected: results narrow to messages currently pinned in their room; composes with the Images/Files/Video chips and room/sender/date filters; toggling off restores results. It also narrows the encrypted/local-cache results section (not just server results). Needs a room with actually pinned messages.

M5. New theme presets (Cyberpunk / Ocean / Blood Red / Classic Matrix / Midnight) — ⚠️ visual judgment

Settings → Appearance → theme picker → try each of the 5 new themes. Expected: each applies a complete, legible dark palette. Code review computed WCAG contrast and all pass AA, but eyeball these specifically: Midnight (lowest-contrast accent #6b7ca8 — selected/focus states), Classic Matrix (green accents, light-green body text on near-black), Blood Red (white-ish text on bright-red buttons). Confirm Success/Warning/Critical (save/leave/delete) still look correctly green/amber/red, not recolored. Switching back to a stock theme should fully revert.


N. OIDC / Next-Gen Auth login (MSC3861) — P4-6

The Lotus client can now sign into OIDC-native homeservers (ones that delegate auth to a Matrix Authentication Service / MAS), e.g. mozilla.org. lotusguild's own server is not MSC3861, so test EITHER against a local MAS dev loop (full setup in dev/oidc-test/README.md — docker-compose + Synapse msc3861 delta + a config.json override) OR against mozilla.org with a real account.

N1. OIDC login flow (the core test) — needs a MAS homeserver

  1. On the login screen, select the OIDC homeserver (local localhost:8008, or mozilla.org).
  2. Expected: instead of the username/password form, a single "Continue with single sign-on" button appears (password + legacy-SSO are suppressed for that server).
  3. Click it → redirected to the provider's login page (MAS / chat.mozilla.org).
  4. Authenticate there → redirected back to …/auth/oidc/callback → a brief "Signing you in…" spinner → you land in the app, logged in.

Expected: no console CSP violations; you reach the room list as the OIDC user.

N2. Session persists across reload (token storage)

After N1, hard-refresh the page. Expected: you stay logged in — the OIDC session (access + refresh token + issuer/clientId/claims) was persisted (cinny_refresh_token, cinny_oidc_* keys in localStorage).

N3. Token refresh (long-lived session)

Leave the session past the access-token lifetime (MAS default is short — or revoke the access token in the MAS admin UI to force a 401). Expected: the client refreshes transparently (no logout); the stored access token rotates (reactive 401 refresh via the wired OidcTokenRefresher).

N4. Logout revokes at the issuer

Log out from Settings. Expected: back to login; OIDC tokens are revoked at the issuer's revocation_endpoint (best-effort) and all cinny_* / cinny_oidc_* keys are cleared. Logging back in works.

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).


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

Unread dot on federated rooms + avatar-decoration console storm (2026-07):

  • Open a room from another homeserver that has thread activity; read it → the room's unread dot clears (previously an unread thread reply kept the dot because markAsRead only sent an unthreaded receipt at the main-timeline tail). Also confirm opening a thread + reading it clears its part of the badge.
  • With DevTools console open on those 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, 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.