Two causes. (1) Three right-hand columns don't fit under ~1500 px: the member
drawer now yields to an open content panel (thread / gallery / widgets /
threads list) on desktops ≤ 1500 px and returns when it closes, like Element.
(2) The thread composer rendered the full ten-button toolbar inside a 360 px
panel, leaving 24 px for the input ('S…') at every width — RoomInput gains a
compactLayout prop (the phone one-row '+ | input | emoji | send' layout) and
ThreadPanel uses it.
Measured headless: 1400 px thread open → drawer hidden, composers 328/168 px
(was 422 → 'Send a mes…' + 24 px); 1600 px → drawer stays, thread composer 168 px.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
ThreadPanel only honoured the separate Private Read Receipts switch, so
the privacy switch still emitted public m.read for every opened thread.
Match markAsRead: hideActivity || privateReadReceipts.
Fixes#34
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
The thread composer reused RoomInput's hardcoded editableName="RoomInput", so
the main timeline's global up-arrow "edit last message" handler fired while
focused in a thread composer and targeted the MAIN timeline's last message
(wrong), and there was no up-arrow edit for the thread itself.
- Make editableName a RoomInput prop (default "RoomInput"); the thread composer
passes "ThreadInput", so the two up-arrow handlers never cross-fire.
- Add an up-arrow-edit handler to ThreadTimeline (parity with RoomTimeline):
empty thread composer + Up -> edit the latest editable reply in that thread,
using thread.liveTimeline + canEditEvent + setEditId.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
Default = Participating: thread replies notify only when you've posted in the
thread or are @mentioned; per-thread override to All / Mentions-only / Mute via
a bell menu in the thread panel header. Modes sync across devices in
io.lotus.thread_notifications account data (pruned on write: left rooms, >180d,
cap 200/room). Muted threads: no notifications/sounds, chip badge suppressed
(+BellMute glyph), and their counts are subtracted from the room's sidebar
badge (client-side; clamped ≥0).
Also fixes the thread notification path itself: thread replies are now owned by
exactly ONE handler (room-level ThreadEvent.NewReply via a new useRoomsListener
hook, with per-thread dedupe, panel-aware focus suppression, and per-thread OS
tag coalescing) — the existing RoomEvent.Timeline handlers in the notifier and
the unread binder are explicitly thread-guarded, eliminating the previously
un-gated/double path. Room badges now also refresh live on
RoomEvent.UnreadNotifications (surgical per-room PUT; fixes thread-badge lag).
Pure decision core shouldNotifyThreadReply (13-case matrix) + prune + unread
subtraction: +32 tests (648 total). E2EE caveat documented: mentions-only may
under-notify pre-decryption (same class as the existing path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two-reviewer audit of the thread stack; confirmed findings fixed:
- ThreadTimeline: wrap encrypted events in EncryptedContent so a live-arriving
E2EE reply re-renders when its key decrypts (decryption emits neither
RoomEvent.Timeline nor ThreadEvent.Update — previously stuck at "Unable to
decrypt").
- ThreadPanel: mark-read deduped on the latest event id (RoomEvent.Timeline
re-emits per backfilled event/edit/reaction; previously up to N receipt POSTs
per panel open) + rejection handled with retry.
- RoomTimeline: ThreadSummary chips now mount only for events carrying thread
data (each chip holds a room-level listener; one per rendered message would
blow the SDK's 100-listener emitter cap) with a single room-level
ThreadEvent.New tick for new-thread liveness.
- useThreadPendingEvents: keep a sent reply visible through the /send-response→
/sync window (was flashing out of the pending strip before landing).
- ThreadTimeline: reseed the window on RoomEvent.TimelineReset (gappy sync left
a detached timeline).
Documented-acceptable (reviewer-noted): thread typing shows as room typing (no
per-thread typing in the spec; Element matches), thread panel + members drawer
can be open together, scheduled-send is thread-unaware but unreachable there.
Gates: tsc clean, eslint 0 errors, build OK, 616/617 tests (1 IDB skip).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Right-side thread drawer (MembersDrawer pattern; mobile fullscreen):
- ThreadPanel: header + close/Escape, ThreadTimeline, its own RoomInput
(threadRootId prop; drafts/replies/uploads isolated per roomId::threadId;
schedule + slash-commands off in threads v1) and threaded mark-as-read.
- ThreadTimeline: lean reimplementation over thread.liveTimeline — copied
useTimelinePagination pattern (/relations back-pagination + decryption),
virtualized, root event emphasized + "N replies" divider, reactions/edits/
redactions, and a pending strip (chronological local echo never enters the
thread timelineSet — rendered from LocalEchoUpdated instead).
- ThreadSummary chips on root messages (server-aggregated bundle or live
Thread; unread badge via getThreadUnreadNotificationCount) keep threads
discoverable now that replies leave the main timeline.
- Reply-in-Thread menu + thread indicators open the panel; deep links to
thread events redirect into it.
- State: roomIdToActiveThreadIdAtomFamily + getThreadDraftKey (+18 tests).
Gates: tsc clean, eslint 0 errors, build OK, 616/617 tests (1 IDB skip).
Awaiting live QA; release note: threaded replies no longer render inline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>