Commit Graph

3700 Commits

Author SHA1 Message Date
jared fd9e4a9802 feat(download): show a toast + button check when a file is saved
The desktop (Tauri) app has no native download UI, so FileSaver.saveAs saved
files silently — no visual or audio confirmation. Users re-clicked because
nothing said it worked (one report: 5 copies of the same file). Add a small
useSaveFile() hook that saves AND raises a 'Downloaded <filename>' toast, and
route every download call site through it (file attachments, image viewer, PDF
viewer, plus the recovery-key / key-backup exports). The file-message download
button also shows a green check on success.

Toast system extended with an optional iconSrc so system toasts render an icon
instead of an avatar/initials, and an empty roomName is no longer rendered.

Tests: createDownloadToast covered; 701/701 pass; typecheck + build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:30:57 -04:00
jared f12175e76f fix(unread): stop stuck/resurrecting read indicators
CI / Build & Quality Checks (push) Successful in 10m53s
CI / Trigger Desktop Build (push) Successful in 9s
handleReceipt recomputed unread from getUnreadNotificationCount, which is
server-computed and stale on the synchronous synthetic receipt echo (the SDK
only zeroes it immediately when the last event is our own message). Reading
someone else's message therefore PUT the stale non-zero count back -> dot stuck
or resurrected on the ack-sync ordering race. Restore upstream cinny's
optimistic DELETE on our own receipt; the UnreadNotifications listener re-asserts
the accurate badge on the server ack.

Also collapse a {total:0,highlight:0} PUT to a DELETE in the reducer (a present
map entry lights the dot via hasUnread=!!unread, so phantom {0,0} PUTs from the
UnreadNotifications listener left stuck dots).

Mark-as-Unread (MSC2867): clear the flag directly in markAsRead (opening an
already-read room sends no receipt, so the receipt-driven auto-clear never
fired), and gate the receipt auto-clear to main/unthreaded receipts so reading
one thread no longer wipes the whole-room flag.

Tests: 700/700 pass; typecheck + prod build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:07:21 -04:00
jared b5db617bd2 docs: log unread/read-receipt flakiness bug (investigating)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:49:52 -04:00
jared 4ecc173554 docs: record remaining spec/MSC gaps survey (buildable vs blocked)
CI / Build & Quality Checks (push) Successful in 10m53s
CI / Trigger Desktop Build (push) Successful in 6s
Full-surface protocol survey. Flags each remaining gap by what unblocks it:
buildable now (custom room tags/sections — the only substantive client-only one
left), needs infra (email/3PID invites → identity server; MSC4108/3814), and
blocked-until-Synapse-upgrade (live location 3489/3672, reaction redaction 3892,
room preview 3266, thread subs 4306). Space reordering already works (drag) — not
a gap. Corrected per user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:51:47 -04:00
jared 44854a1529 docs: park Sliding Sync (evaluated — not viable for a safe rollout)
CI / Build & Quality Checks (push) Successful in 10m41s
CI / Trigger Desktop Build (push) Successful in 6s
Three research passes concluded ~10% confidence a full rollout wouldn't
break/regress (js-sdk SlidingSync is _internal_/experimental + labs-only at
Element, presence not delivered over sliding sync, no upstream Cinny reference,
and Cinny's nav is built from the full local room set — ~14 subsystems assume
completeness). Server side is GA. Parked; revisit on Rust SDK adoption or large
accounts. Full assessment in the plan history.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:45:44 -04:00
jared 43f4ceb45d feat(rooms): Room Widgets (MSC1236 im.vector.modular.widgets)
Phase C.1 of the protocol-gaps roadmap, gate-green (693 tests). Generalizes the
Element Call widget host into a general room-widget feature:
- StateEvent.Widget + widgetsPanelAtom + useRoomWidgets (WidgetParser).
- RoomWidgetView: sandboxed-iframe host via ClientWidgetApi with a conservative
  GeneralWidgetDriver (approves only benign display caps — no room-event
  send/read/to-device). Blocks same-origin widget URLs (sandbox breakout guard).
- WidgetsPanel: list / open / add / remove, PL-gated on im.vector.modular.widgets,
  https + non-same-origin URL validation. Mounted like the media gallery (header
  toggle + 3-way content-panel exclusivity + mobile full-screen overlay).
- Tested URL/capability/id helpers.

Requires the prod CSP frame-src widening (matrix repo) for external widgets.
v1 cuts (capability consent prompt, Jitsi/sticker types, user widgets) noted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:27:23 -04:00
jared 17bd50cc4e feat(crypto): QR-code device verification (alongside emoji SAS)
CI / Build & Quality Checks (push) Successful in 11m7s
CI / Trigger Desktop Build (push) Successful in 7s
B2 of the Matrix protocol-gaps roadmap, gate-green (688 tests):
- Enable QR verification methods (show/scan/reciprocate) in initMatrix.
- Extend DeviceVerification: the Ready step offers your own QR (byte-mode encode
  via qrcode), a camera 'Scan their QR code' flow, and an emoji fallback; the
  Started step routes reciprocate → a confirm step (useVerifierShowReciprocateQr)
  or SAS as before.
- New QrScanner component: getUserMedia + jsQR, handing the raw binaryData bytes
  to request.scanQRCode (BarcodeDetector is string-only, so can't be used).
- Adds qrcode + jsqr (small, pure-JS, client-only); build-verified under rolldown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:30:23 -04:00
jared 82e52e1bc7 feat(rooms): Disappearing Messages (MSC1763 m.room.retention)
B1 of the Matrix protocol-gaps roadmap, gate-green (688 tests):
- StateEvent.RoomRetention + a shared utils/retention.ts (presets, isExpired,
  getRoomRetentionMs) with tests.
- RoomRetention settings control (PL-gated preset buttons Off/1d/1w/1m) in Room
  Settings → General → Message Retention.
- Timeline hides events past the room's max_lifetime (gated behind Show Hidden
  Events, like redactions) — messages visually disappear, losslessly.
- Opt-in setting enforceRetentionLocally (default OFF) + a headless
  RetentionSweeper that permanently redacts the user's OWN expired messages
  (own-only, loaded-timeline scope, dedupe + retry). Nothing auto-deletes unless
  the user opts in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:23:14 -04:00
jared d46b91b1b8 feat(rooms): Mark as Unread (MSC2867) + Low Priority rooms
Two Matrix protocol gaps (Phase A), gate-green (683 tests):
- Mark as Unread: m.marked_unread room account data (+ com.famedly.marked_unread
  fallback), a new markedUnreadAtom binder that seeds from account data and
  clears on our own read receipt (MSC2867). RoomNavItem gains Mark as Unread /
  Read menu items and lights the row dot for a marked room. Tested.
- Low Priority: m.lowpriority room tag mirroring favourites — a context-menu
  toggle (mutually exclusive with Favorite) and a collapsed Low Priority
  category sorted to the bottom of the Home room list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 00:04:47 -04:00
jared 5b94a44eb3 docs: add Matrix Protocol Gaps backlog (audited spec/MSC gaps)
Six confirmed client-buildable gaps + server-gated items from a spec/MSC audit:
Mark as Unread (MSC2867), Low Priority rooms (m.lowpriority), Disappearing
Messages (MSC1763), QR Device Verification, Room Widgets (MSC1236), Sliding Sync
(MSC3575/4186). Phased build order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:53:33 -04:00
jared ca9abb5363 docs: condense LOTUS_TODO to open work only (1063→~230 lines)
CI / Build & Quality Checks (push) Successful in 10m37s
CI / Trigger Desktop Build (push) Successful in 7s
Removed resolved audit-wave finding tables and shipped-feature narratives (now
in LOTUS_FEATURES.md + git history); kept every open/blocked/deferred item, the
E2EE + Web Push backlog, and the reference tables (server caps, key files, EC
fork ops, CI/CD).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:23:03 -04:00
jared 21276a47fc fix(audit): low-tail cleanup — session/logout/unread/presence/forward
CI / Build & Quality Checks (push) Successful in 10m45s
CI / Trigger Desktop Build (push) Successful in 14s
Clears the clean 🟡 remainders from the feature audit (gate-green, 677 tests):
- F3: getFallbackSession prefers the session-blob/legacy source with the later
  expiresAt (a downgrade→upgrade could boot on a stale blob's dead token).
- F6: server-forced logout (SessionLoggedOut) now mirrors logoutClient —
  pushSessionToSW() + best-effort revokeOidcTokens for OIDC sessions (the search
  plaintext wipe was already added).
- N5: deleteUnreadInfo parent fallback `?? roomId` → `?? []` (latently spread the
  roomId string into chars).
- P10: useUserPresence re-seeds when the User object appears after first render.
- forward: strip m.mentions so forwarding doesn't re-ping the original mentions.

Left open: F5 (OIDC expiry not reachable in persistTokens), N6/H10/D7 (minor /
runtime-verify). See LOTUS_TODO.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:57:09 -04:00
jared b7788cc79c docs: mark D6 Windows rich-toast AUMID fixed + add runtime test
CI / Build & Quality Checks (push) Successful in 10m41s
CI / Trigger Desktop Build (push) Successful in 7s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:32:31 -04:00
jared 13d08c3fd7 docs: mark H5 invite-QR fixed (local generation)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:19:42 -04:00
jared a899d7d3a8 fix(privacy): generate invite QR locally instead of api.qrserver.com (H5)
The Share Room QR was fetched from the third-party api.qrserver.com, leaking
which rooms a user shares (and failing offline / under strict CSP). Now rendered
locally via qrcode.react (QRCodeSVG) — no network request, works offline. Added a
white quiet-zone container so the code scans on any theme; dropped the qrError
fallback (local generation can't fail the same way). Removed api.qrserver.com
from the prod CSP img-src (matrix repo). Build verified (rolldown interop OK).
Verification steps added to LOTUS_TESTING.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:19:22 -04:00
jared dcd8201e16 fix(wave-3): audit fixes — ACL guards, presence, moderation, theming perf
Wave-3 bug-hunt fixes (findings in LOTUS_TODO), reviewed + gate-green:
- 🔴 ACL editor [H1–H4]: block saving an empty allow-list (was a one-click
  federation brick), warn on self-ban (case-insensitive glob match of
  mx.getDomain() vs allow/deny), accept real globs (1.2.3.*, *.evil.*), and
  gate Save behind a confirm dialog.
- 🔴 [P1] room context menu no longer acts on the wrong room after a live
  reorder (key by roomId, not list index). 🔴 [P2] status writes no longer
  force presence to online over Invisible/DND (shared presenceStateFromSetting).
- 🟠 [P3] timed mutes restored on boot; [P4] custom-status auto-clear now fires
  (always-mounted StatusExpiryMonitor); [P5] timezone also PUT to the m.tz
  profile field so it's visible to others; [H6] RoomInsights single-pass
  min/max (was Math.min(...spread) stack overflow); [H7/H8] mod-log labels.
- 🟡 [P6/P7] favorites collapse+filter, [P8] charCount reset, [P9] DM preview
  refresh on decrypt; theming [T-P1] lazy decorations, [T-P2] drop the redundant
  always-on body animation, [T-P4] live useReducedMotion, [T-P5] decoration key.
- NATIVE-CINNY LAW: notification presets + Powers permissions use folds icons.

DEFERRED: [H5] invite-QR is fetched from api.qrserver.com (third-party leak);
local generation needs a bundled QR lib (not added). tsc/eslint/prettier clean,
build OK, 677 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:40:07 -04:00
jared 41149db685 fix(ui): NATIVE-CINNY LAW — replace emoji with folds icons in settings
- Notification profile presets (P5-27) used literal emoji (🎮/💼/🌙) instead of
  folds Icons → Gaming=Ball, Work=Monitor, Sleep=BellMute.
- Permissions "Powers" list used / text emoji for has/no-power → folds
  Icons.Check / Icons.Cross (colored via the row).

Reviewed the rest of the UI: seasonal-theme picker emoji kept (folds has no
holiday-icon equivalents; a distinctly-Lotus visual feature), soundboard clip
emoji kept (user-chosen clip identity), URL-preview brand glyphs + upstream
device-verification emoji + keyboard key-symbols left as-is.

(Also records the F2 URL-preview decision: keep default-on.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:21:00 -04:00
jared 668bdaad7d fix(wave-2): audit fixes — account-data races, search-cache wipe, export, media
Web fixes from the Wave-2 bug-hunt (findings in LOTUS_TODO):
- F1 (security): wipe the decrypted-plaintext search index on SERVER-FORCED
  logout too (token expiry / remote sign-out) — only manual logout did before.
  F4: the delete no longer reports success while onblocked (waits, 3s cap).
- M1/M2 (data-loss): useBookmarks + useUserNotes account-data writes are now
  serialized at MODULE scope (single queue + latestRef per client, echo-driven),
  fixing the cross-instance lost-update clobber (useBookmarks mounts per message
  row, so a per-instance queue was insufficient — caught in review).
- M6: room-history export gets a 200-page cap + Cancel + unmount-abort +
  correct date-range early-break (raw paginated ts). M4: image compression
  skips PNG (was flattening transparency to black), bakes EXIF orientation via
  createImageBitmap, .jpg-renames, and falls back to the original on decode
  failure instead of dropping the file. M5: MediaGallery lightbox opens the
  right item (shared thumb guard). M8: audio speed survives async decrypt.
- Desktop web wiring: D2 badge sums leaf rooms only (space double-count, like
  the favicon fix); D3 useTauriDnd re-hydrates from get_tray_dnd on mount; D5
  updater has a terminal state.

Reviewed; M7 reverted (past-time clamp is an intentional, tested contract).
tsc/eslint/prettier clean, build OK, 678 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:56:27 -04:00
jared ee6bdd8241 fix(call): Wave-1 audit fixes (calls host side)
- C-H1: forceState only on FIRST join; on EC reconnect re-arm the fork handlers
  (resendForkState — deafen+quality only) instead of clobbering live mic/video/
  deafen back to the join-time snapshot.
- C-H2: AFK auto-mute reads the fork's io.lotus.call_state VAD of the LOCAL
  published track instead of getUserMedia on the browser DEFAULT mic (which could
  measure silence while the user spoke on another device → auto-mute an active
  speaker). Fails safe (never mutes) when call_state is null OR empty.
- C-H3: control observer re-binds after EC re-renders (body subtree:true + 100ms
  debounce) with an early-return so unchanged state doesn't re-render.
- C-M3 setQuality join-gated; C-M4 hangup 4s fallback dispose (idempotent);
  C-M5 PTT no longer silently un-deafens; C-M6 screenshare-audio mute resets on
  stop; C-L4 deafen key works in the iframe; C-L6 setState-after-unmount guards.

Reviewed (C-H2 [] fail-safe + C-H3 re-render guard applied). tsc/eslint/prettier
clean, build OK, 677 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:20:07 -04:00
jared 0bbdd7ce94 fix(notifications/threads): Wave-1 audit fixes (🔴 + web 🟠)
- T1 (🔴): markThreadAsRead no longer receipts the thread ROOT (a 2nd instance
  of the read-marker-corruption regression — opening a thread whose root is old
  re-lit the whole room). Extracted to a pure threadReceipt.ts + 5 regression
  tests.
- N1 (🔴): favicon/tab-title unread count now sums only leaf rooms (was double-
  counting every ancestor-space aggregate in roomToUnread).
- N2 (🔴): notifications/sounds dedupe on the event id, not the unread count —
  fixes "read a DM, next message never notifies again".
- T4 (🟠): the thread notification path no longer re-gates on the room count, so
  an explicit per-thread "All replies" override in a Mentions-only room fires.
- N3 (🟠): getUnreadInfos skips phantom {0,0} entries (muted-thread-only rooms no
  longer light the nav row / pollute unread filters).
- N4 (🟠): the Receipt handler recomputes unread instead of blanket-DELETE, so a
  threaded receipt can't wipe a room's valid main-timeline badge.
- T2 (🟠): thread "Jump to Latest" re-anchors the virtual window (was landing on
  a stale mid/old event).

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:10:32 -04:00
jared 7c85ad177f docs(audit): Wave-1 bug-hunt findings (notifications/threads/calls/EC fork)
4 parallel deep-audit agents over the Tier-1 high-risk areas. Findings only (no
source changes). Top 🔴: markThreadAsRead corrupts the main read marker via a
thread-root receipt (a SECOND instance of the P6 read-receipt regression, likely
a live cause of "unread won't clear"); favicon/title count double-counts space
aggregates; deliverNotification dedupe cache never cleared on read → missed
notifications/sounds. Plus 🟠 (thread "All" override defeated, phantom
muted-thread dot, receipt-DELETE badge race, thread jump-to-latest, call
forceState-on-reconnect clobber, AFK wrong-mic auto-mute, stale control observer)
and a long 🟡 tail. Recorded in LOTUS_TODO for prioritized fix passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:25:57 -04:00
jared bbf0800c19 fix(ci): disable lines-between-class-members + prefer-arrow-callback for test files
CI / Build & Quality Checks (push) Successful in 10m46s
CI / Trigger Desktop Build (push) Successful in 12s
CI check:eslint failed with 28 errors in two test files: callSounds.test.ts
(lines-between-class-members on mock classes) and lotusDenoiseUtils.test.ts
(prefer-arrow-callback on `function AudioWorkletNode(){}` constructor mocks —
arrows aren't constructable, so auto-fixing would break the test). Both are
stylistic false-positives for test code; relax them in the existing test-file
override next to max-classes-per-file. `npm run check:eslint` now exits 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 18:27:01 -04:00
jared abd0753148 fix(notifications): safe thread receipts on mark-read (fixes read-receipt regression)
CI / Build & Quality Checks (push) Failing after 35m56s
CI / Trigger Desktop Build (push) Has been skipped
The prior thread-receipt change (8192da5a) broke read receipts globally. Exact
cause: markAsRead used `thread.lastReply() ?? thread.rootEvent`. When a thread's
replies weren't loaded (lastReply() null — common on room open), it sent a
receipt for the thread ROOT. Since roots are "in the main timeline",
threadIdForReceipt() makes that a MAIN receipt at an old event; when the root
isn't in the loaded timeline the SDK's backward-guard falls back to timestamp
and applies it, moving the main read receipt onto an event we don't have, so
getEventReadUpTo() returns null and roomHaveUnread() reports the room unread —
re-broken on every mark-read, amplified by the bulk mark-all-orphan-rooms-read
callers.

Fix: main unthreaded receipt unchanged; the thread loop now sends a threaded
receipt ONLY for a genuine loaded thread reply (thread.lastReply()), never the
root — if replies aren't loaded, skip. New notifications.test.ts locks the
regression (null lastReply → no root receipt) + the main/threaded/no-op cases.

Gates: tsc/eslint/prettier clean, build OK, 672 tests (7 new).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:09:28 -04:00
jared 8192da5a12 fix(notifications): clear thread receipts on mark-read; cap avatar-decoration refetch
CI / Build & Quality Checks (push) Successful in 10m41s
CI / Trigger Desktop Build (push) Successful in 29s
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
jared 6dc478e989 fix(desktop): Custom Window Chrome toggle breaks the timeline (P5-47)
CI / Build & Quality Checks (push) Successful in 10m35s
CI / Trigger Desktop Build (push) Successful in 9s
Toggling custom chrome expanded the screen and sent the message feed
auto-scrolling into the past. Two causes:
- DesktopChrome used height:100vh while html/#root use 100dvh; in the Tauri
  webview 100vh can exceed the visible height after decorations are stripped,
  making the timeline's scroll container taller than the viewport → the virtual
  paginator runs away paginating backwards. Switched to 100dvh.
- Toggling live reflowed the whole app while the timeline was mounted. The
  setting now persists + reloads so the layout is rebuilt cleanly (description
  updated: "reloads to apply").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:16:52 -04:00
jared 049472e25f feat(crypto) + docs: request persistent storage; consolidate docs to 3
CI / Build & Quality Checks (push) Successful in 10m54s
CI / Trigger Desktop Build (push) Successful in 12s
- index.tsx: request navigator.storage.persist() for logged-in sessions so the
  browser can't evict the IndexedDB rust-crypto store (eviction while the
  localStorage session survives resurrects the device with a blank store → the
  KE-1 "one time key already exists" upload storm). Guarded, checks persisted()
  first, best-effort.
- Docs: remove HANDOFF_ELEMENT_CALL_FORK.md, LOTUS_E2EE_INVESTIGATION.md, and
  LOTUS_BUGS.md. Port their live content into the three kept docs — verification
  backlog → LOTUS_TESTING; open bugs + E2EE (KE-1..4) + an Element Call fork
  operational reference (publish steps + io.lotus action catalog) → LOTUS_TODO.
  Fix all dangling references (README, code comments, cross-doc links). Full
  history of the removed docs remains in git.

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:28:09 -04:00
jared 81904372bc docs(e2ee): investigation update — 41.7.0 delta + web-specific KE-1 root cause
CI / Build & Quality Checks (push) Successful in 10m49s
CI / Trigger Desktop Build (push) Successful in 21s
Code-read + upstream-issue triage this session:
- 41.7.0 / crypto-wasm 18.3.1 does NOT fix KE-1 (no OTK/upload change; #5200
  still open) — the SDK-pin remediation lever is closed.
- Confirmed root cause = rust-crypto store <-> Synapse OTK divergence; the
  leading web trigger is that cinny never requests persistent storage, so the
  IndexedDB crypto store is evictable while the localStorage session survives.
- New buildable preventive mitigation: navigator.storage.persist() on login
  (+ multi-tab guard, 400-loop recovery prompt). Added as §6 with a secondary
  KE-2 to-device-validation hypothesis and capture discriminators.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:14:46 -04:00
jared c82ab5c7f5 chore(contrib): security headers in example nginx/caddy configs (P6-4)
Add HSTS + Permissions-Policy + the standard X-Frame/X-Content/Referrer set to
the contrib nginx (443 block) and caddy examples; fix the caddy SPA try_files
fallback (stray space). Generic (no homeserver-specific CSP). The real prod
config lives in the matrix repo. P6-4 trimmed to headers only — patch-package /
types-drift / build-config skipped (see LOTUS_TODO).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:41:08 -04:00
jared ebcd8ec926 feat(ux): forward to multiple rooms + live bookmark previews (P6-3)
Forward: checkbox multi-select room picker + "Send to N rooms" batch send
(Promise.allSettled). Full success auto-closes; partial failure keeps the dialog
open with a "Forwarded to X/N — failed: …" summary and prunes the selection to
only the failures (retry won't duplicate to already-sent rooms). Content builder
extracted to a unit-tested forwardContent.ts (edit-forwarding, reply-strip,
undecryptable-refused; 4 tests).

Bookmarks: BookmarksPanel resolves each saved message's live event (useRoomEvent)
so previews reflect edits and show a deleted indicator for redactions; the stored
snapshot stays as the fallback while loading, on fetch failure, or after leaving
the room. Stored bookmark shape unchanged.

Gates: tsc/eslint/prettier clean, build OK, 665 tests. Reviewed (dup-resend on
retry + Checkbox readOnly fixed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:30:33 -04:00
jared 4ff07ea2bd feat(call): send io.lotus.set_deafen to the fork (P6-2 phase 1)
CallControl now sends the new io.lotus.set_deafen action (join-gated via
forceState) on every deafen / screenshare-audio-mute toggle + on join, ALONGSIDE
the retained iframe-DOM .muted hack (transitional). Against the current pinned
bundle the action is immediately error-replied + swallowed by .catch — inert, no
timeout. Reordered toggleSound() to commit state before setSound() so the sent
deafen value isn't inverted.

Phase 2 (after the fork is published): bump the pin lotus.1 -> lotus.2 and delete
the DOM hack. Docs: HANDOFF §12.4, LOTUS_TODO P6-2, LOTUS_BUGS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:12:08 -04:00
jared 804caa5130 feat(desktop): tray Do-Not-Disturb + Launch-on-login toggle (P6-1 web)
- useTauriDnd + manualDndAtom: the native tray "Do Not Disturb" toggle
  (lotus-dnd-changed event) OR's into the notification quiet-gate in
  ClientNonUIFeatures (both invite + message notifiers), alongside Focus Assist.
- AutostartSetting in Settings → General (desktop-only): reads/sets
  plugin:autostart via invoke. Mirrors the window-chrome setting.
- Docs: LOTUS_FEATURES desktop section (Linux parity + DND + autostart),
  LOTUS_TODO P6-1 → [~], LOTUS_BUGS verification row.

Gates: tsc/eslint/prettier clean, build OK, 661 tests. Native side committed on
cinny-desktop:main (CI-compile-pending).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:31:09 -04:00
jared 625f0c2386 docs(todo): add P6 post-audit batches (desktop parity, EC DOM-hack retirement, web UX, hygiene)
macOS explicitly out of scope; Linux is the parity target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:13:29 -04:00
jared 4d7a05c0f1 fix(a11y): review-wave fixes (P3-4)
CI / Build & Quality Checks (push) Successful in 11m3s
CI / Trigger Desktop Build (push) Successful in 22s
- `?` shortcut now stopImmediatePropagation so RoomView's type-to-focus handler
  doesn't steal focus into the composer behind the dialog (and swallow Escape) —
  CONFIRMED review finding.
- Typing live region stays mounted (empty when idle) so the FIRST "X is typing"
  is reliably announced (a status region added with its text isn't always read).
- Removed a stray empty `{}` JSX expression in MediaGallery (leftover from an
  auto-fix).

Reviewer verified the rest: collapsed-message labels, focus-return
classification (4 dialogs fixed, popouts correctly left), and all aria fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:57:32 -04:00
jared b5e7bcc0b8 chore: prettier-normalize page/style.css.ts (pre-existing debt)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:50:32 -04:00
jared bca371ad38 feat(a11y): label the moderation reason input (P3-4)
Missed from the form-labels commit — aria-label on the shared kick/ban/invite
reason input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:46:35 -04:00
jared 899a14c119 docs: P3-4 accessibility — features section, TODO/BUGS, LOTUS_TESTING §P
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:45:22 -04:00
jared 6728a1274d chore(a11y): enforce a curated jsx-a11y lint gate in CI (P3-4)
Enables ARIA-correctness rules (aria-props/proptypes/role/unsupported-elements,
role-has/supports-aria-props, no-redundant-roles, anchor/heading-has-content)
+ label-has-associated-control as errors — a regression gate for accessible
names + valid ARIA. control-has-associated-label deliberately NOT enabled (the
repo's <Text as="label" htmlFor> component pattern defeats its static analysis);
the real gaps it surfaced were fixed directly. Also disable max-classes-per-file
for test files (mock classes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:45:22 -04:00
jared 21dda93d1b feat(a11y): focus return, typing announcement, shortcuts help (P3-4)
- Focus returns to the trigger when closing 4 genuine dialogs (room-topic
  viewer, reaction viewer, header topic, Search) — 20 inline popouts/menus
  correctly left as-is (returning focus to a hover target would be wrong).
- Typing indicator announced via a visually-hidden role="status" region;
  the visual text is aria-hidden to avoid double announcement.
- New keyboard-shortcuts help dialog (press ?, ignored while typing),
  mounted in ClientNonUIFeatures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:45:22 -04:00
jared 4380041014 feat(a11y): label form controls + overlays (P3-4)
Accessible names for ~15 controls that lacked them: invite/join/create-room/
account-data/image-pack/private-note/power-level inputs (visible <label htmlFor>
where a label exists, else aria-label); the two range sliders (night-light
intensity, noise-gate threshold); the soundboard file input; media <video>
elements; and the Media Gallery (region) + Search (dialog) overlays. Hidden
notification/preview <audio> marked aria-hidden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:45:21 -04:00
jared 8729ccfcf5 feat(a11y): message semantics for screen readers (P3-4)
- Each message is role="article"; collapsed messages (consecutive from one
  sender) now carry an aria-label with sender + time — previously a screen
  reader heard only the body with no attribution (the biggest a11y gap).
  Pure messageAriaLabel() reuses the existing time utils (+3 tests).
- Editing a message announces "Editing message from <sender>" (ariaLabel
  threaded MessageEditor → CustomEditor; the main composer is unaffected).
- System emoji get role="img" + aria-label from the shortcode; custom
  emoticons always have an accessible name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:45:21 -04:00
jared 8ab1ec254b docs(testing): add July batch — threads, per-thread notifs, math, search cache, session, audit wave, desktop CSP (§O)
Fills the gap where LOTUS_BUGS referenced test IDs (P3-8/P4-1/P4-4/P4-8/N97a/
AW-1..4) with no matching procedures in the testing guide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:15:48 -04:00
jared 23f715857c docs: mark P4-8 (search cache) + session-atomicity as shipped
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:09:50 -04:00
jared f589182709 docs: deep-audit wave dispositions in LOTUS_BUGS
CI / Build & Quality Checks (push) Successful in 10m57s
CI / Trigger Desktop Build (push) Successful in 7s
Dep triage recorded (zero shipped exposure; SDK now 41.7.0 stable; dompurify
removed); Needs Verification rows for the audit-wave fixes (scheduled-cancel,
emoji lazy-load, SW precache, desktop CSP smoke).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:19:50 -04:00
jared ef573376ac chore(deps): matrix-js-sdk 41.6.0-rc.0 → 41.7.0 stable
Off the release candidate onto stable: pulls matrix-sdk-crypto-wasm 18.3.1 (a
security update) + MSC4140 delayed-event auth fixes. Thread/receipt API
signatures spot-checked unchanged (sendEvent threadId overloads, sendReceipt
unthreaded arg). Gates green: tsc/build/658 tests. E2EE runtime behavior needs
the usual live smoke (send/receive in an encrypted room, call keys).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:19:21 -04:00
jared 34d9272790 feat(call): denoise asset smoke check at ML-tier call start
HEAD-checks the copied denoise worklet/wasm/model assets for the selected model
and console.warns a single line listing anything missing — a silent asset skew
between the EC fork's expectations and vite's copied files would otherwise
disable noise suppression with no signal. Fire-and-forget; never blocks call
setup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:19:16 -04:00
jared 96f7187031 perf(audit): emojibase lazy-split, SW precache, Prism subset, lazy images
- emojibase (~965 KB) is now fully lazy: plugins/emoji.ts loads compact data +
  shortcode maps via a memoized dynamic import (rejections reset the memo so a
  mid-deploy chunk 404 can retry); reaction labels degrade to the raw glyph
  until loaded. Consumers get FRESH array references on load (the module arrays
  populate in place — same-ref state updates would skip re-render and leave
  emoji search empty; reviewer-caught). Verified out of the eager graph.
- Service worker precaches hashed assets (workbox precacheAndRoute, 82 entries
  ~10.8 MB incl. the crypto wasm): repeat visits stop re-downloading the app.
  index.html is NOT precached — navigations stay network-first so deploys are
  picked up immediately; the media-auth fetch handler is untouched.
- ReactPrism: curated 21-language set — chunk 574 KB → 71 KB.
- Timeline inline images get loading="lazy".
- Removed dead dompurify (+types); sanitize-html is the real sanitizer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:19:16 -04:00
jared 664dcd4cd8 fix(audit): correctness wave — ghost sends, Escape coordination, panel exclusion
- ScheduledMessagesTray: cancel prunes local state ONLY on confirmed server
  cancel; failures keep the item + show an inline error (was: a failed cancel
  looked cancelled but still sent at the scheduled time).
- Escape semantics: the composer consumes Escape (preventDefault+stopPropagation)
  iff autocomplete is open or a reply draft is set; the thread panel and Room's
  markAsRead act only on unconsumed Escape, and markAsRead defers entirely while
  a thread panel is open (listener order made it fire before the panel closed).
- Room: thread panel / media gallery are mutually exclusive (most-recently-
  opened wins); on mobile at most one right panel renders (thread > gallery >
  members) instead of stacked fullscreen overlays.
- RemindMeDialog: busy-disabled presets (no more double-click duplicates),
  try/catch with inline error, close only on success.
- ThreadTimeline: "Jump to Latest" floating chip when scrolled up (RoomTimeline
  idiom).

From the 4-auditor deep-audit wave; reviewer-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:18:51 -04:00
jared 7f960b026b fix(build): complete the threadSummary rename — remove the old casing
CI / Build & Quality Checks (push) Successful in 10m44s
CI / Trigger Desktop Build (push) Successful in 7s
The deletions from the git-mv in 992d2b83 were unstaged by a concurrent
worktree operation before commit, so the pushed tree contained BOTH
threadSummary.ts and threadSummaryData.ts (and the Windows case-collision
persisted). This commit removes the stale originals; caseCollision.test.ts
would have failed CI on the incomplete state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 23:44:59 -04:00
jared 992d2b83b3 fix(build): rename threadSummary.ts — case-collision broke the Windows release
CI / Build & Quality Checks (push) Failing after 5m22s
CI / Trigger Desktop Build (push) Has been skipped
threadSummary.ts (pure helpers) and ThreadSummary.tsx (chip component) lived in
the same directory differing only by case. On the case-insensitive Windows
release runner, RoomTimeline's extensionless import of ./thread/ThreadSummary
resolved .ts BEFORE .tsx and matched the helper module → rolldown
MISSING_EXPORT "ThreadSummary" — invisible on every Linux/macOS build (and the
cause of the earlier masked pdf.worker failure). Helper module renamed to
threadSummaryData.ts (+ test), 3 importers updated.

Prevention: new caseCollision.test.ts walks src/ and fails on any same-directory
names differing only by case (extensionless compare, so Foo.tsx vs foo.ts is
caught) — verified it fails on the pre-rename tree. Runs in the hard CI gate.

Gates: tsc clean, eslint/prettier clean, build OK, 658/659 tests (1 IDB skip).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 23:43:20 -04:00
jared a9505ca5b2 feat(soundboard): shared room/space packs (like emoji/stickers), grid picker, management
CI / Build & Quality Checks (push) Successful in 10m56s
CI / Trigger Desktop Build (push) Successful in 8s
Soundboard v2 — a near-parallel of the custom-emoji image-pack system for
in-call audio clips.

- Data model: 3-tier packs mirroring MSC2545 — room/space pack (state event
  io.lotus.soundboard, inherited by child rooms via parent-space aggregation),
  global refs (io.lotus.soundboard_rooms), and the personal pack
  (io.lotus.soundboard account data; the v1 flat-list content is migrated to the
  pack shape on read). New plugins/soundboard/ (readers, SoundboardPack, utils) +
  hooks/useSoundboardPacks (useRelevantSoundboardPacks = user U global U room,
  deduped). Unit-tested (migration + slug).
- Management: reusable SoundboardPackEditor (name + emoji + per-clip volume +
  delete + upload + batched save), power-level-gated for room packs like emoji
  packs; a Soundboard page wired into Room + Space settings.
- In-call: CallSoundboard rewritten as a Discord-style grid grouped by pack
  (emoji + name tiles), sourcing room+parent-space U personal clips; a Manage
  toggle embeds the editors; per-clip volume x master volume on playback.
- Spam guard: host gates on a playing key (fork enforces one clip at a time).
- Control bar: Mute-Screenshare moved next to the Screenshare button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 23:21:50 -04:00