Skip to content

feat(showcase): load older messages + per-message permalinks - #764

Merged
samxu01 merged 1 commit into
mainfrom
feat/showcase-history-and-permalinks
Jul 27, 2026
Merged

feat(showcase): load older messages + per-message permalinks#764
samxu01 merged 1 commit into
mainfrom
feat/showcase-history-and-permalinks

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Why

Two gaps in the public read-only room, both of which matter because that room is how we show the product to people who can't log in:

  1. No way to read history. hasMore was already tracked in state and rendered as a dead note telling visitors older messages existed — with no control to load them.
  2. No way to link to a specific message. A read-only showcase link is for pointing someone at a particular exchange; you could only send them to the room and hope.

What

Load older messages — a real button, using the before cursor the showcase API already accepted. No backend change was needed; GET /api/showcase/:podId/messages?before=&limit= was already implemented and clamped.

Per-message permalinks — every message is an anchor (#msg-<id>) with a hover control that copies ?m=<id>. On load, if the target isn't in the current window, the page walks backwards until it finds it — bounded by MAX_AUTO_PAGES so a stale link can't page an entire room into memory — then centres it and flashes a highlight. The initial auto-scroll-to-bottom is suppressed when a target is present, or it would yank the visitor away from the message they were sent to.

The trap this had to avoid

The 12-second poll replaced the whole message list. Naively adding pagination would have made loaded history silently vanish on the next tick.

Both paths now merge by id and re-sort chronologically. And only the initial fetch and loadOlder may write hasMore — letting a poll write it would resurrect the button after a visitor had already reached the beginning of the room, because the flag describes the newest page's tail rather than how far back we've actually paged.

Verification

Real browser against the live showcase room (via a local CORS proxy, since jsdom can't verify scroll or paging):

  • Load older: 49 → 98 messages on click, oldest msg-51152msg-50855, newest msg-51293 still present
  • Permalink: ?m=50855 auto-paged back two pages, found the target, scrolled it into the viewport
  • Checked at 390px; the control and button both render correctly, and the zh-CN string renders ("加载更早的消息")
Test Suites: 68 passed, 68 total
Tests:      310 passed, 310 total

Two new regression tests cover the before cursor + prepend + merge-survives-poll path, and the permalink page-back-and-highlight path. Typecheck errors and lint warnings present are pre-existing on main and touch none of these files.

Note

zh-CN strings are included and follow the existing glossary, but 🇨🇳 a native pass is the merge gate, same as #763.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RWkf1SuYRHosxnPqsXwaVa

The public room told visitors older messages existed and gave them no way to
reach them -- `hasMore` was tracked in state and rendered as a dead note. And
there was no way to point someone at a specific exchange, which is the whole
job of a read-only showcase link.

Load older: the button uses the `before` cursor the showcase API already
accepted. No backend change was needed.

Permalinks: every message is an anchor with a hover control that copies
?m=<id>. On load, if the target is not in the current window the page walks
backwards until it finds it, bounded by MAX_AUTO_PAGES so a stale link cannot
page an entire room into memory, then centres it and flashes a highlight.
The initial auto-scroll-to-bottom is suppressed when a target is present, or
it would yank the visitor away from the message they were sent to.

One trap worth naming: the 12s poll replaced the whole message list, so
naively adding pagination would have made loaded history vanish on the next
tick. Both paths now merge by id and re-sort. Only the initial fetch and
loadOlder may write hasMore -- letting a poll write it would resurrect the
button after the visitor reached the beginning.

Verified in a real browser against the live room through a local proxy:
49 -> 98 messages on click with the newest retained, and ?m=50855 paged back
two pages, found the target and scrolled it into view at 390px width.
310/310 frontend tests pass, including two new regression tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWkf1SuYRHosxnPqsXwaVa
@samxu01
samxu01 merged commit 35fdbb6 into main Jul 27, 2026
10 checks passed
lilyshen0722 added a commit that referenced this pull request Jul 28, 2026
"Load older messages" worked on the anonymous showcase route but did nothing in
a pod you had actually joined. Not a broken feature — it was never built there.
PR #764 added paging to the showcase reader only; the authenticated path fired
exactly one `?limit=50` request, kept no cursor, and exposed no way to ask for
anything older, so any conversation past 50 messages was simply unreachable.

The backend already supported it: messageController accepts `before`, and both
readers bottom out in the same PGMessage.findByPodId. The entire gap was
client-side.

- useV2PodDetail gains hasMore / loadingOlder / loadOlder. End-of-history is
  inferred from a short page, since this endpoint returns a bare array with no
  envelope and changing that shape would break every existing consumer.
- Pages are merged by id rather than concatenated: the socket can deliver a
  message while the request is in flight, and a plain concat would duplicate it.
- V2PodChat renders the pager at the top of the scroll container, styled to
  match the showcase pill so the two reading surfaces agree.

Two scroll defects had to be fixed or the feature would have looked broken:

- the auto-scroll effect keyed on `messages.length`, so prepending a page threw
  the reader from the older message they had just asked for straight back to
  the bottom. It now keys on the newest message's id, so prepends are ignored.
- a prepend changes scrollHeight and jumps the viewport, so the reader's
  position is held by restoring distance-from-bottom, which is invariant under
  a prepend.

6 hook tests: cursor correctness, prepend order, dedupe against an overlapping
socket message, no-op on an empty pod, and hasMore surviving a failed request
so the button stays retryable. Frontend suite 69/69, 316 tests. Lint at
baseline.

Needs a real-browser check after deploy — jsdom has no layout engine, so the
scroll-anchor behaviour specifically is not covered by these tests.


Claude-Session: https://claude.ai/code/session_01DMeWzgFxsfBcjoLVLewEES

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants