feat(ui): per-conversation draft sidebar indicators + caret restore (LC-237) #297
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-237-draft-sidebar-indicators"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements LC-237 (per-conversation draft persistence with sidebar draft indicators) on top of the existing LC-64 server-side draft store, rather than adding the parallel client-side store the issue text originally proposed. LC-64 already persists drafts per (user, room) for both rooms and DMs, restores them on open, and clears them on send/schedule/kick/account-delete, so reusing it keeps a single source of truth and makes the indicator cross-device. This covers subtasks LC-238 (persist + caret restore) and LC-239 (sidebar indicator); LC-240 is the QA pass.
Sidebar draft indicator (LC-239)
A pencil indicator appears next to any room/DM that holds a fresh unsent draft.
db::drafts::room_ids_with_draftsreturns the set of rooms with a non-empty, non-stale draft (60-day freshness mirrors the render-sideget_fresh_or_purgepurge).load_sidebarsetshas_drafton eachSidebarRoom/SidebarPeer.partials/draft_badge.htmlrenders an id-keyed span#lc-draft-{room_id}(room id for rooms, dm_room_id for DMs) carrying the pencil when a draft exists and an empty span otherwise, so a later OOB swap always has a target.ChatEvent::DraftChanged { user_id, room_id, has_draft }viabroadcast_to_userfrom the three draft commit points (debounced draft PUT upsert / empty-body delete, clear-on-send infinalize_message_send, clear-on-schedule). Rendered per recipient throughws/draft_badge.html, gated to the owner. Clears are gated onrows_affectedso an ordinary no-draft send does not fan a redundant OOB to every tab.Caret-position restore (LC-238)
The draft body is already restored server-side (
initial_draft). This adds the one piece the server cannot cheaply persist: the caret/selection offset.localStoragekeyed by conversation id, so it survives reloads and PWA restarts. Device-local by design: the text is the cross-device state, the caret is an ephemeral per-device convenience.autofocusviarequestAnimationFrame, clamped to the live text length (the cross-device body may have changed). Dropped when a successful send clears the textarea.Not in scope / deviations
[operator-action]marker.Tests
room_ids_with_drafts_returns_only_fresh_nonempty_rows(fresh/stale/cross-user set semantics, no purge-on-read).sidebar_shows_draft_pencil_for_room_with_draft/sidebar_omits_draft_pencil_when_no_draft.just testandjust test-saasboth green; clippy + fmt clean.🤖 Generated with Claude Code
Surface a pencil indicator in the sidebar next to any room/DM that holds an unsent draft, and restore the composer caret position when reopening a conversation. Builds on the existing LC-64 server-side draft store (message_drafts) rather than adding a parallel client-side one, so there is a single source of truth and the indicator is cross-device. Sidebar indicator (LC-239): load_sidebar reads the set of rooms with a fresh draft (db::drafts::room_ids_with_drafts, 60-day freshness mirroring the render-side purge) and sets has_draft on each SidebarRoom/SidebarPeer. A shared partials/draft_badge.html renders an id-keyed span (#lc-draft-{room_id}) carrying the pencil when a draft exists, empty otherwise so a later OOB swap always has a target. Live updates ride a new ChatEvent::DraftChanged broadcast_to_user from the three commit points (debounced draft PUT upsert/empty-delete, clear-on-send in finalize_message_send, clear-on-schedule), rendered per recipient via ws/draft_badge.html and gated to the owner; clears are gated on rows_affected so a no-draft send does not fan a redundant OOB. Caret restore (LC-238): the draft body is already restored server-side via initial_draft; this adds the one piece the server cannot cheaply persist, the caret/selection offset, keyed by conversation id in localStorage so it survives reloads and PWA restarts. Device-local by design (text is the cross-device state, caret is an ephemeral per-device convenience). Restored on mount past autofocus via rAF and clamped to the live text length; dropped when a successful send clears the box. Tests: room_ids_with_drafts set semantics (fresh/stale/cross-user, no purge-on-read), and sidebar render with and without the pencil. just test + just test-saas green. #LC-237 #LC-238 #LC-239 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>