Skip to content

fix(desktop): add support for deeplinking to channel/messages#759

Merged
wesbillman merged 5 commits into
mainfrom
copy-link-to-messages
May 27, 2026
Merged

fix(desktop): add support for deeplinking to channel/messages#759
wesbillman merged 5 commits into
mainfrom
copy-link-to-messages

Conversation

@wesbillman
Copy link
Copy Markdown
Collaborator

What

Polishes the sprout://message autolinker added earlier in this branch and fixes one real bug surfaced in review.

Bug fix — trailing punctuation eaten by URL match.
Bare sprout://message?… URLs at end-of-sentence were matched whole, including the trailing . / , / ; / : / ! / ?, so parseMessageLink produced e.g. messageId: "abcdef." and the pill routed to a nonexistent message instead of the copied event id. The match now peels off trailing sentence punctuation and unmatched closing brackets, emitting them as plain text after the pill.

Structural cleanup.
The fix initially landed as a duplicate tree-walker in remarkMessageLinks.ts. Folded it back into createRemarkPrefixPlugin so the factory now:

  • accepts { node, trailing } from its builder (mentions/channels still return a bare node — legacy path preserved);
  • skips link / code / inlineCode nodes once, on behalf of all three plugins.

Net effect: remarkMessageLinks.ts is 36 lines, no duplicated walker.

Files

  • desktop/src/shared/lib/createRemarkPrefixPlugin.ts{ node, trailing } return shape; centralized skip list.
  • desktop/src/features/messages/lib/remarkMessageLinks.ts — trim helper + builder, no walker.
  • desktop/src/shared/ui/markdown.test.mjs — cases for . , ; : ! ?, paren wrapping, no-trailing sanity.

Verification

  • node --test --experimental-strip-types src/shared/ui/markdown.test.mjs
  • Full desktop test suite (302 pass) ✅
  • pnpm typecheck, pnpm check, pnpm build
  • Beth's adversarial Codex pass: APPROVE.

Deferred

Wes asked whether the pill should show channel name + message snippet instead of #channel · 6char. Three options were considered (cache-only snippet, async fetch with skeleton, channel-only). Decision: keep current channel + 6-char shape for now; revisit when there's a story for fetching the linked message body synchronously at render time.

Adds a Slack-style "Copy link" action to message rows that produces a
`sprout://message?channel=<uuid>&id=<eventId>[&thread=<rootId>]` URL.
Pasted/typed sprout:// links render as clickable in-app links and route
through the existing channel route's scroll-into-view + getEventById
backfill. The same scheme is also handled at the OS level via the
already-registered Tauri deep-link plugin.

- New `features/messages/lib/messageLink.{ts,test.mjs}` — build/parse
  helpers + 10 round-trip / validation tests.
- `MessageActionBar` (+ `MessageRow`, `InboxMessageRow`/`InboxDetailPane`)
  — Copy link dropdown item, gated on a `channelId` prop so callers
  without it (rare) silently hide the action.
- `shared/ui/markdown.tsx` — `a` override branches on
  `isMessageLink(href)` and dispatches to `useAppNavigation.goChannel`.
  http(s) behavior unchanged.
- `features/messages/lib/useRichTextEditor.ts` — adds `protocols: ["sprout"]`
  so TipTap's URL sanitiser doesn't strip the scheme on paste/typed input.
- `src-tauri/src/lib.rs` — `Some("message")` arm in `handle_deep_link_url`
  emits `deep-link-message` with a JSON payload (channelId, messageId,
  threadRootId).
- `shared/deep-link.ts` — adds `listenForMessageDeepLinks` +
  `MessageDeepLinkPayload`. New `shared/useMessageDeepLinks.ts` hook
  wires it into the router from inside `AppShell` (kept out of `App.tsx`
  because `useAppNavigation` requires the router tree).

Routing simplification: the brief allowed skipping forum-vs-stream
detection if not trivially detectable. Rather than guess from
`threadRootId`, both the in-app and OS handlers always go through
`goChannel` with `messageId`; the channel route's existing infra
resolves the target whether it's a stream reply or a forum thread.
`threadRootId` is still parsed and preserved end-to-end for future use.

Verified: pnpm typecheck, pnpm check (biome + file-size), 289 node:test
units (incl. 10 new), pnpm build, just desktop-tauri-fmt-check, just
desktop-tauri-check, cargo test --lib (374 passed).

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
…k params

Beth-flagged review issues from PR #copy-link-to-messages:

1. react-markdown's defaultUrlTransform strips unknown schemes to ""
   before the <a> component override sees them, breaking copy → paste →
   click for sprout://message?… end-to-end. Add a urlTransform that
   passes message-link hrefs through unchanged and delegates everything
   else to defaultUrlTransform (so javascript: etc. stay safe). Covered
   by 5 new tests in markdown.test.mjs that render real <ReactMarkdown>
   and assert on the emitted href.

2. The Tauri sprout://message arm in lib.rs accepted empty values:
   sprout://message?channel=&id=foo would emit channelId: "" to the
   frontend. Filter empty params on the way in. Extracted the parse
   logic into parse_message_deep_link so it can be unit-tested without
   a live AppHandle; added 6 unit tests including a regression for the
   empty-channel case (NIT #4 from the review).

3. Verified cross-workspace fallback in ChannelRouteScreen: when the
   linked channel isn't in the active workspace, channelsQuery settles,
   activeChannel is null, ChannelScreen renders <ChannelScreenEmptyState/>
   ("Select a channel to view messages.") — no spinner-loop, no crash.
   Already graceful, no toast needed.

4. Documented threadRootId in messageLink.ts as reserved for future
   "open in thread view" routing (NIT #5).

File-size override for lib.rs bumped 715→730 with a precise note for
the parse helper extraction + 6 unit tests.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
The existing inlined copy + comment was reused from the markdownUtils
pattern, but messageLink.ts has no React dependency so it can be
imported directly. Removes one source of drift between markdown.tsx
and its test.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
remark-gfm only autolinks http(s)://, so bare sprout://message URLs in
message text rendered as raw 100-char strings. Add remarkMessageLinks
plugin (built on the existing createRemarkPrefixPlugin factory, mirroring
remarkChannelLinks/remarkMentions) that replaces matched URLs with a
custom message-link HAST element. markdown.tsx renders that as a
clickable pill (#channel · 6-char-id), routed through the existing
onOpenMessageLink handler.

Malformed URLs render as plain text rather than misleading clickable
pills. Trailing punctuation and whitespace terminate the match.

Plugin uses the explicit .ts extension on its factory import so the file
is consumable from both Vite (markdown.tsx) and node --test
--experimental-strip-types (markdown.test.mjs).

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
…age URLs

Bare `sprout://message?…` URLs at end-of-sentence (e.g. "See sprout://…abc.")
were being matched whole, including the trailing `.`, which made
`parseMessageLink` produce `messageId: "abc."` and route to a nonexistent
message instead of the copied event id.

Peel trailing sentence punctuation (`. , ; : ! ?`) and unmatched closing
brackets off each match before emitting the message-link node; the trimmed
characters are appended as plain text. Implemented by extending
`createRemarkPrefixPlugin` to accept `{ node, trailing }` from its builder
so `remarkMessageLinks` doesn't need its own walker — the same factory now
handles mentions, channels, and message links uniformly, including
link/code/inlineCode skipping which was previously per-plugin.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman wesbillman requested a review from a team as a code owner May 27, 2026 18:22
@wesbillman
Copy link
Copy Markdown
Collaborator Author

Screenshot 2026-05-27 at 11 23 50 AM

@wesbillman wesbillman changed the title fix(desktop): trim trailing punctuation from autolinked sprout://message URLs fix(desktop): add support for deeplinking to channel/messages May 27, 2026
@wesbillman wesbillman merged commit 8946653 into main May 27, 2026
15 checks passed
@wesbillman wesbillman deleted the copy-link-to-messages branch May 27, 2026 19:27
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.

1 participant