feat(composer): #channel-link autocomplete and clickable links (LC-323) #338

Merged
longjacksonle merged 1 commit from feat/lc-323-channel-link-autocomplete into main 2026-06-17 02:32:00 +02:00

What

#channel-link autocomplete: type # in the composer to autocomplete a room in the current enclave you can access; the chosen #name token renders as a clickable link to that room (/room/{id}), styled like a mention chip. A direct parallel to the existing @-mention and :emoji: autocomplete. Implements LC-323 / LC-324.

How

  • Autocomplete. New GET /rooms/{id}/channel-complete?q= (access-gated like emoji-complete) returns a #lc-channel-popover listbox of accessible same-enclave rooms, prefix-ranked, capped at 8; rows carry data-insert="#name". A composer JS block mirrors the emoji trigger (#-anchored regex, htmx.ajax, cursor insert).
  • Resolution helper. routes::channel_refs_for_room = enclave_for_room + list_rooms_in_enclave (already access-scoped: public, or private-and-member; admin sees all) filtered to the linkable charset. Empty for DMs / non-enclave rooms.
  • Render. MessageView gains a channels: Vec<ChannelRef> (computed once per page/DM render, cloned per message; fetched per-render in the WS single-message paths). body_html calls a new markdown::render_with_channels, which threads the channel set through render_inner / render_with_spoilers / math::render_math_in_text into render_body. There the boundary-anchored @ scan is generalized to (^|\s)([@#])([A-Za-z0-9_-]{1,64}); the # branch emits a /room/{id} link when the lowercased token resolves, else falls through to the literal-text pass. linkify_body / emojify_and_escape are untouched, and # inside markdown link labels is deliberately not resolved (no nested anchors).
  • Untouched paths. The 3-arg markdown::render delegates with an empty channel set, so wiki / description / scheduled / edit-history render exactly as before. The channel set is folded into markdown_cache_key, so a cached body never serves a stale link across enclaves.

Scope / assumptions

  • Intra-enclave (names are unique per enclave); DMs / non-enclave rooms resolve nothing, and the autocomplete returns empty there, keeping insertion and render consistent.
  • Linkable charset [A-Za-z0-9_-]{1,64}, matched case-insensitively. Rooms with spaces / other characters are not #-linkable in v1, so the autocomplete only offers names that match (a user is never offered a #name that won't render).
  • Render-time resolution (no post-time table): renames / access changes reflect on the next render; unresolved or inaccessible #token renders as plain text (no leak about rooms the viewer can't see).

Testing

  • just check (clippy standalone + saas, fmt) clean.
  • just test and just test-saas green. New views::room unit tests cover #name -> link, case-insensitive match with canonical-name display, unknown token literal, empty-channel-set literal, and mid-word non-match. New routes_channel_complete.rs covers prefix match, empty-query listing, the non-linkable-name filter, the access-gated 403, and the non-enclave-room empty case.

Operator impact

None. No new env var, config, or contract change; no [operator-action] marker.

🤖 Generated with Claude Code

## What `#channel`-link autocomplete: type `#` in the composer to autocomplete a room in the current enclave you can access; the chosen `#name` token renders as a clickable link to that room (`/room/{id}`), styled like a mention chip. A direct parallel to the existing `@`-mention and `:emoji:` autocomplete. Implements LC-323 / LC-324. ## How - **Autocomplete.** New `GET /rooms/{id}/channel-complete?q=` (access-gated like emoji-complete) returns a `#lc-channel-popover` listbox of accessible same-enclave rooms, prefix-ranked, capped at 8; rows carry `data-insert="#name"`. A composer JS block mirrors the emoji trigger (`#`-anchored regex, `htmx.ajax`, cursor insert). - **Resolution helper.** `routes::channel_refs_for_room` = `enclave_for_room` + `list_rooms_in_enclave` (already access-scoped: public, or private-and-member; admin sees all) filtered to the linkable charset. Empty for DMs / non-enclave rooms. - **Render.** `MessageView` gains a `channels: Vec<ChannelRef>` (computed once per page/DM render, cloned per message; fetched per-render in the WS single-message paths). `body_html` calls a new `markdown::render_with_channels`, which threads the channel set through `render_inner` / `render_with_spoilers` / `math::render_math_in_text` into `render_body`. There the boundary-anchored `@` scan is generalized to `(^|\s)([@#])([A-Za-z0-9_-]{1,64})`; the `#` branch emits a `/room/{id}` link when the lowercased token resolves, else falls through to the literal-text pass. `linkify_body` / `emojify_and_escape` are untouched, and `#` inside markdown link labels is deliberately not resolved (no nested anchors). - **Untouched paths.** The 3-arg `markdown::render` delegates with an empty channel set, so wiki / description / scheduled / edit-history render exactly as before. The channel set is folded into `markdown_cache_key`, so a cached body never serves a stale link across enclaves. ## Scope / assumptions - **Intra-enclave** (names are unique per enclave); DMs / non-enclave rooms resolve nothing, and the autocomplete returns empty there, keeping insertion and render consistent. - **Linkable charset** `[A-Za-z0-9_-]{1,64}`, matched case-insensitively. Rooms with spaces / other characters are not `#`-linkable in v1, so the autocomplete only offers names that match (a user is never offered a `#name` that won't render). - **Render-time resolution** (no post-time table): renames / access changes reflect on the next render; unresolved or inaccessible `#token` renders as plain text (no leak about rooms the viewer can't see). ## Testing - `just check` (clippy standalone + saas, fmt) clean. - `just test` and `just test-saas` green. New `views::room` unit tests cover `#name` -> link, case-insensitive match with canonical-name display, unknown token literal, empty-channel-set literal, and mid-word non-match. New `routes_channel_complete.rs` covers prefix match, empty-query listing, the non-linkable-name filter, the access-gated 403, and the non-enclave-room empty case. ## Operator impact None. No new env var, config, or contract change; no `[operator-action]` marker. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(composer): #channel-link autocomplete and clickable links (LC-323)
All checks were successful
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 8s
check-secrets / Kingfisher (push) Successful in 18s
check-secrets / Nosey parker (pull_request) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 9s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 3m36s
0df7d1f70d
Type # in the composer to autocomplete a room in the current enclave that you can access; the chosen #name token renders as a clickable link to that room. A direct parallel to the existing @-mention and :emoji: autocomplete.

The render integration folds into the single author-render pipeline: a new `markdown::render_with_channels` threads a channel set through render_inner / render_with_spoilers / math into `render_body`, whose boundary-anchored @ scan is generalized to `(^|\s)([@#])([A-Za-z0-9_-]{1,64})`. The # branch resolves the token (lowercased) against the message's same-enclave room map and emits a /room/{id} link; unresolved or inaccessible tokens stay literal text. The 3-arg `markdown::render` is unchanged (delegates with an empty channel set) so wiki / description / scheduled / edit-history are untouched, and the channel set keys the markdown cache so a body never serves a stale link across enclaves.

`#name` is intra-enclave (names are unique per enclave); DMs and non-enclave rooms resolve nothing. A channel token is the linkable charset `[A-Za-z0-9_-]{1,64}`, so the autocomplete only offers rooms whose names fully match it (insertion and render stay consistent). Resolution and autocomplete both honor `list_rooms_in_enclave` access scoping. en/es parity.

#LC-323
#LC-324

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/lc-323-channel-link-autocomplete 2026-06-17 02:32:01 +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!338
No description provided.