feat(ui): forward a message to another room or DM (LC-278) #317

Merged
longjacksonle merged 1 commit from feat/lc-278-message-forwarding into main 2026-06-14 16:58:02 +02:00

What

Adds a "Forward" hover action on messages (LC-278). It opens a destination picker - the viewer's post-able rooms + their DM conversations, filterable, excluding the source - and forwarding reposts the message into the chosen destination with a "Forwarded from " attribution, appearing live there.

Per the scoping decision: v1 carries text + attribution (not attachments); destinations are rooms + DMs.

How

  • New routes::forward:
    • GET /messages/{id}/forward renders the picker modal (rooms via list_rooms + DMs via list_user_dm_rooms, peer labels resolved, blocked peers and the source conversation excluded). Source read-access is gated.
    • POST /messages/{id}/forward/{dest_room_id} builds the forwarded body as a Markdown blockquote (attribution header + the original body, each line quoted), caps to MAX_MESSAGE_CHARS (LC-153), insert_messages it, and broadcasts via the existing finalize_message_send so it fans out like a normal post. Gated end to end and re-checked on POST: read access on the source; banned/muted, room access, can_post_with_policy, and is_blocked_either_way for DM destinations.
  • message.html gains the Forward button (after Remind); layout.html gets a singleton #lc-forward-modal slot. The picker mirrors the reminders modal (focus trap, Escape/backdrop close, confirm fragment) and reuses the LC-274-style data-attr filter.
  • Strings localized in en + es.

A dedicated forwarded_from schema column was rejected for v1 - the blockquote attribution reuses the entire post/render/broadcast path with zero schema change. No new env. Not operator-visible.

Tests

routes_forward: forward lands in the destination with attribution; destination access gate returns 403; picker lists destinations and excludes the source room. The four LC-77 golden fixtures are regenerated for the new Forward button (diff is exactly that one button). just test and just test-saas pass.

QA note

Server-gated and test-covered. The picker + confirm are JS-driven, so worth a manual pass: Forward a message, filter the list, pick a room (confirmation shows, the message appears live in that room with the "Forwarded from" quote); pick a DM; confirm Escape/backdrop/Close dismiss; confirm forwarding into a read-only or inaccessible room is refused; confirm the source conversation is absent from the list.

## What Adds a "Forward" hover action on messages (LC-278). It opens a destination picker - the viewer's post-able rooms + their DM conversations, filterable, excluding the source - and forwarding reposts the message into the chosen destination with a "Forwarded from <author>" attribution, appearing live there. Per the scoping decision: v1 carries **text + attribution** (not attachments); destinations are **rooms + DMs**. ## How - New `routes::forward`: - `GET /messages/{id}/forward` renders the picker modal (rooms via `list_rooms` + DMs via `list_user_dm_rooms`, peer labels resolved, blocked peers and the source conversation excluded). Source read-access is gated. - `POST /messages/{id}/forward/{dest_room_id}` builds the forwarded body as a Markdown blockquote (attribution header + the original body, each line quoted), caps to `MAX_MESSAGE_CHARS` (LC-153), `insert_message`s it, and broadcasts via the existing `finalize_message_send` so it fans out like a normal post. Gated end to end and re-checked on POST: read access on the source; banned/muted, room access, `can_post_with_policy`, and `is_blocked_either_way` for DM destinations. - `message.html` gains the Forward button (after Remind); `layout.html` gets a singleton `#lc-forward-modal` slot. The picker mirrors the reminders modal (focus trap, Escape/backdrop close, confirm fragment) and reuses the LC-274-style data-attr filter. - Strings localized in en + es. A dedicated `forwarded_from` schema column was rejected for v1 - the blockquote attribution reuses the entire post/render/broadcast path with zero schema change. No new env. Not operator-visible. ## Tests `routes_forward`: forward lands in the destination with attribution; destination access gate returns 403; picker lists destinations and excludes the source room. The four LC-77 golden fixtures are regenerated for the new Forward button (diff is exactly that one button). `just test` and `just test-saas` pass. ## QA note Server-gated and test-covered. The picker + confirm are JS-driven, so worth a manual pass: Forward a message, filter the list, pick a room (confirmation shows, the message appears live in that room with the "Forwarded from" quote); pick a DM; confirm Escape/backdrop/Close dismiss; confirm forwarding into a read-only or inaccessible room is refused; confirm the source conversation is absent from the list.
feat(ui): forward a message to another room or DM (LC-278)
All checks were successful
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 6s
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Kingfisher (pull_request) Successful in 6s
check-secrets / Nosey parker (pull_request) Successful in 6s
Check / clippy + fmt + tests (pull_request) Successful in 2m13s
Create release / Create release from merged PR (pull_request) Has been skipped
c0d98649cf
Adds a "Forward" hover action. It opens a destination picker - the viewer's post-able rooms plus their DM conversations, filterable, excluding the source - and reposting into the chosen destination drops the message there with a "Forwarded from <author>" attribution, appearing live.

v1 carries the text body + attribution (not attachments, per the scoping decision). The forward is built as a Markdown blockquote (attribution header + the original body, each line quoted), length-capped (LC-153), inserted via insert_message and broadcast through the existing finalize_message_send so it fans out exactly like a normal post. Gated end to end and re-checked on POST (the picker list is convenience, not the boundary): read access on the source; banned/muted, room access, posting policy, and a block check for DM destinations. The picker mirrors the reminders modal (focus trap, Escape/backdrop close, confirm fragment) and reuses the LC-274-style data-attr filter.

A dedicated forwarded-from schema column was rejected for v1: the blockquote attribution reuses the whole post/render/broadcast path with no schema change. The four LC-77 golden fixtures are regenerated for the new Forward button (diff is exactly that button). Strings localized in en + es. Tests cover a successful forward (lands in the destination with attribution), the destination access gate (403), and the picker (lists destinations, excludes the source).

#LC-278
#LC-279

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/lc-278-message-forwarding 2026-06-14 16:58:03 +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!317
No description provided.