feat/message-grouping #30

Merged
nrupard merged 13 commits from feat/message-grouping into main 2026-05-04 21:11:06 +02:00
Owner
No description provided.
Slack-style grouping: messages from the same author within 5 minutes render as a single visual group, hiding the username/timestamp header on follow-ups while keeping reactions, edit/delete (hover-revealed), and seen indicators per message. Includes promote-on-delete to repair the visual chain when a header is removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure helpers for the upcoming consecutive-message grouping feature, with full unit-test coverage of the boundary conditions (same/different user, exact-5-minute boundary, gap exceeded, no prior message). The constant is exposed in seconds because chrono::Duration constructors aren't const.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Field plumbed through every MessageView construction site - room loader, DM loader, WS new-message render, WS edited-message render, get_single_message, patch_message. All sites set false for now; subsequent commits compute the real value at the appropriate places.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The room and DM page loaders now walk messages chronologically with a running prev pointer and stamp each MessageView with the correct is_follow_up flag. Added prior_message_in_room and next_message_in_room helpers in db::chat for the upcoming WS broadcast and promote-on-delete paths. Integration tests cover the chronological pass and both helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When MessageView.is_follow_up is true the username/timestamp header is suppressed and vertical padding shrinks from py-2 to py-0.5. Edit/Delete actions move out of the header into an absolutely positioned hover overlay so authors and moderators can still reach them on follow-ups. Reactions, the body, and the seen-caption slot remain rendered on every message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The WebSocket new-message and edited-message renderers now look up the prior message in the same room and compute the grouping flag, so consecutive messages from one author render as a follow-up live without a page reload. The single-message and patch handlers in routes/room.rs do the same so the cancel-edit and post-edit fragments preserve the grouping state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new ChatEvent::MessageRegrouped variant routed through the existing edited-message render path. The DELETE handler now looks up the next message in the room before soft-deleting; if that next message was a follow-up of the deleted header (same author, within the grouping window), it broadcasts a regrouping event so each connected viewer re-renders that message as a header. Tests cover both the promote case and the lone-message no-promote case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Saves the implementation plan alongside the spec and applies the rustfmt-required line breaks to the chained-method calls in message_grouping tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops follow-up vertical padding from py-0.5 to py-0, applies leading-snug to message bodies, and removes the top margin on the reactions wrapper for follow-ups (with empty:hidden so an empty wrapper does not contribute its own line height). Header messages keep py-2 and mt-1 so the boundaries between authors stay clearly separated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the read state only advanced on GET /dm/{peer} or GET /room/{id} (page load). If a user was already inside a DM and the peer sent a message over WebSocket, the message rendered into the open room but the viewer's last_read_message_id stayed at the old value. As a result the peer's "Seen HH:MM" caption never updated until the viewer left and re-entered the DM.

The WS new-message handler now treats arrival in a foreground subscription as an implicit read: it calls set_last_read for the viewer and broadcasts a DmRead event so the author sees the live "Seen" update and any other tabs of the viewer clear their sidebar badge. Self-authored messages skip the marking since the sender's POST handler already mark-as-reads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The chat scroll container now starts at the newest message on first paint and stays pinned to the bottom whenever the viewer is parked there. Sampling htmx:beforeSwap and htmx:oobBeforeSwap captures the at-bottom state before each swap; afterSwap and oobAfterSwap restore the bottom position when needed. A 50px slack absorbs sub-pixel rounding. Users who have scrolled up to read history are not yanked - the swap stays where they are because wasAtBottom is false.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both the room and DM page handlers now read the viewer's last-read watermark from dm_read_state BEFORE marking the latest message as read, then walk the chronological message list and stamp the first message strictly above the watermark with show_unread_divider = true (skipping own-authored messages so the viewer's own send does not produce a divider on their next visit).

The message template renders a horizontal "Unread messages" banner above the flagged message when the flag is set. The auto-scroll partial now anchors the initial scroll on the divider when one is present, otherwise it falls back to jump-to-bottom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom: when sending messages quickly, some appear to be dropped. Root cause: the composer used outerHTML to replace itself with a fresh fragment from the server. Between submit and swap, the input still held the user's first message. Anything they typed during the in-flight POST landed in that same DOM input and was discarded when the swap replaced the form. To the user this looked like the second message was never sent.

The form now uses hx-swap=none and clears the input on htmx:beforeRequest (which fires after the FormData has already been gathered for the request, so the cleared value does not affect the body). The composer DOM element is no longer replaced, so any keystroke between submits lands in the same persistent input and survives until the user presses Enter again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nrupard deleted branch feat/message-grouping 2026-05-04 21:11:06 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!30
No description provided.