feat(realtime): live refresh affordance for /inbox and /activity (LC-179) #224

Merged
nrupard merged 2 commits from feat/lc-179-live-inbox-activity into main 2026-05-26 20:40:39 +02:00
Owner

Closes LC-179, the final surface of the LC-156 live-updates epic. /inbox (infinite-scroll) and /activity (tab-filtered) carry per-connection view-state the WS server cannot see, so a full-list OOB swap would clobber scrolled pages or the wrong tab. Per the chosen approach, each page gets a lightweight refresh affordance (the standard pattern for paginated/filtered live lists): a bar that is hidden on load, revealed over the WS when new content arrives, and reloads the current URL on click (preserving inbox ordering / the active activity tab via location.reload()).

Changes

  • New InboxRefreshFragment / ActivityRefreshFragment: tiny OOB reveals that swap a shown variant over #lc-inbox-refresh / #lc-activity-refresh. The id exists only on the respective page, so connections elsewhere drop the swap.
  • /inbox: render_new_message_or_bump appends the inbox reveal to its existing background-recipient (unread-badge) branch - reuses the current fan-out, no new broadcast, no per-message full re-render (the amplification the issue warned about).
  • /activity: the Mentioned arm (recipient) appends the activity reveal, regardless of toast-mute (a muted mention still enters the activity feed). Reuses the existing broadcast_to_user(mentioned_user).

Scope note

The activity reveal fires on mentions (the activity event that has a per-user broadcast). Replies/reactions to one's own messages also populate the feed but have no per-user event today, so they don't trigger the bar; a future per-user reply/reaction signal could extend it. Reminders aren't in the activity feed, so they don't reveal it.

Verification

just test and just test-saas both green (106 test result: ok each, zero failures); cargo clippy / cargo fmt clean. Regression tests (routes_inbox::inbox_has_hidden_refresh_bar, routes_activity::activity_has_hidden_refresh_bar) assert both pages render the refresh bar hidden on load.

Closes LC-179, the final surface of the LC-156 live-updates epic. `/inbox` (infinite-scroll) and `/activity` (tab-filtered) carry per-connection view-state the WS server cannot see, so a full-list OOB swap would clobber scrolled pages or the wrong tab. Per the chosen approach, each page gets a lightweight **refresh affordance** (the standard pattern for paginated/filtered live lists): a bar that is hidden on load, revealed over the WS when new content arrives, and reloads the current URL on click (preserving inbox ordering / the active activity tab via `location.reload()`). ## Changes - New `InboxRefreshFragment` / `ActivityRefreshFragment`: tiny OOB reveals that swap a shown variant over `#lc-inbox-refresh` / `#lc-activity-refresh`. The id exists only on the respective page, so connections elsewhere drop the swap. - `/inbox`: `render_new_message_or_bump` appends the inbox reveal to its existing background-recipient (unread-badge) branch - reuses the current fan-out, no new broadcast, no per-message full re-render (the amplification the issue warned about). - `/activity`: the `Mentioned` arm (recipient) appends the activity reveal, regardless of toast-mute (a muted mention still enters the activity feed). Reuses the existing `broadcast_to_user(mentioned_user)`. ## Scope note The activity reveal fires on mentions (the activity event that has a per-user broadcast). Replies/reactions to one's own messages also populate the feed but have no per-user event today, so they don't trigger the bar; a future per-user reply/reaction signal could extend it. Reminders aren't in the activity feed, so they don't reveal it. ## Verification `just test` and `just test-saas` both green (106 `test result: ok` each, zero failures); `cargo clippy` / `cargo fmt` clean. Regression tests (`routes_inbox::inbox_has_hidden_refresh_bar`, `routes_activity::activity_has_hidden_refresh_bar`) assert both pages render the refresh bar hidden on load.
feat(realtime): live refresh affordance for /inbox and /activity (LC-179)
All checks were successful
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 4s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 5s
Check / clippy + fmt + tests (pull_request) Successful in 1m42s
44c8b15278
Final live-updates surface. /inbox (infinite-scroll) and /activity (tab-filtered) carry per-connection view-state the WS server cannot see, so a full-list OOB swap would clobber scrolled pages or the wrong tab. Instead each page gets a lightweight "refresh" bar (the standard pattern for paginated/filtered live lists): hidden on load, revealed over the WS when new content arrives, and clicking reloads the current URL (preserving the inbox ordering / the active activity tab via location.reload()).

- New InboxRefreshFragment / ActivityRefreshFragment: tiny OOB reveals that swap a shown variant over #lc-inbox-refresh / #lc-activity-refresh. The id exists only on the respective page, so connections elsewhere drop the swap.
- /inbox: render_new_message_or_bump appends the inbox reveal to its existing background-recipient (unread-badge) branch - reuses the current fan-out, no new broadcast, no per-message full re-render.
- /activity: the Mentioned arm (recipient) appends the activity reveal, regardless of toast-mute (a muted mention still enters the activity feed). Reuses the existing broadcast_to_user(mentioned_user).

Scope note: the activity reveal fires on mentions (the activity event that has a per-user broadcast). Replies/reactions to one's own messages also populate the feed but have no per-user event today, so they don't trigger the bar; a future per-user reply/reaction signal could extend it. Reminders are not in the activity feed, so they don't reveal the bar.

Regression tests assert both pages render the refresh bar hidden on load (the WS reveal un-hides it).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
docs: bring live-updates documentation current with the LC-156 epic (LC-179)
All checks were successful
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 3s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 6s
check-secrets / TruffleHog (push) Successful in 7s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 1m33s
c73b4f6a33
Per LC-179's closing instruction (document all changes since the last doc update). The whole live-updates buildout (LC-156/160/161/170-179 + the LC-176 hub change) shipped since docs were last touched (ui-conventions.md in LC-169).

- CLAUDE.md: rewrote the WebSocket Flow section to match the current hub (connections + rooms/topics/user_conns indexes, the four fan-out methods, per-recipient render in the send task) and added a "Live updates by default" subsection covering declarative subscription, the two fan-out shapes, id-keyed self-limiting OOB regions, shared partials, the standalone gating rule for admin surfaces, topic cleanup on access loss, and the refresh-affordance exception for paginated pages.
- docs/ui-conventions.md: added a "Live updates" section - the step-by-step recipe a new mutable page follows to be live by construction (extract partial + stable id, OOB fragment, broadcast_to_user vs broadcast_to_topic, per-recipient render), plus the why behind id-keyed OOB and the paginated/admin/access-loss gotchas.
- live-updates design doc: added a "Shipped" addendum mapping the design to the issues that delivered it and recording the two deviations (id-keyed OOB regions instead of a generic merge wrapper; refresh affordance for paginated/filtered surfaces).

Docs only; no code change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-179-live-inbox-activity 2026-05-26 20:40:39 +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!224
No description provided.