Skip to content

Messaging improvements#3737

Open
marcusljf wants to merge 5 commits intomainfrom
messaging-composing
Open

Messaging improvements#3737
marcusljf wants to merge 5 commits intomainfrom
messaging-composing

Conversation

@marcusljf
Copy link
Copy Markdown
Collaborator

@marcusljf marcusljf commented Apr 10, 2026

Composing messages

  • When link text is added or a link has been added, the user can hover over the link to get a tooltip preview of the link.
  • When using the keyboard arrow keys to navigate the message, the link button now indicates that it's being used.
  • When clicking the link, instead of opening it in a new tab, it will now re-open the link editor.
  • Also discovered an issue that if you didn't include the HTTP as part of the URL, it appended the full app.dub.co URL to the link
  • Added CMD+K shortcut to add links
  • Can edit the linked text as well while creating
  • Added modal to create and edit the link

Reading messages

  • When a link has been sent in a message, the user can now hover on the link and the tooltip will show the link preview.
CleanShot.2026-04-13.at.15.24.46.mp4

Summary by CodeRabbit

  • New Features

    • Link hover tooltips show link targets in messages and while editing (only when link support + editing enabled).
    • Clicking a link in the editor opens an in-place link editor modal with Save/Delete and Ctrl/Cmd+K shortcut.
    • URL input is normalized (adds secure protocol when missing) for safer links.
  • Bug Fixes

    • Links without an href continue to render normally.

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Apr 13, 2026 10:57pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

Adds link hover tooltips for rendered markdown and the rich-text editor; introduces editor link editing state with capturing click handling to open a link editor modal; refactors toolbar link flow to a modal-driven editor with URL normalization and keyboard shortcut support.

Changes

Cohort / File(s) Summary
Markdown renderer
apps/web/ui/messages/message-markdown.tsx
Anchor renderer now conditionally wraps links with LinkHoverTooltip when an href exists; falls back to plain <a> when href is absent.
Rich text area public surface & tooltip
packages/ui/src/rich-text-area/index.tsx, packages/ui/src/rich-text-area/...
Exports LinkHoverTooltip; adds RichTextAreaLinkTooltip that tracks hovered <a href> in the TipTap editor DOM and renders a portal tooltip showing the hovered href.
Rich text context & editor behavior
packages/ui/src/rich-text-area/rich-text-provider.tsx
Adds linkEditorOpen / setLinkEditorOpen to RichTextContext; sets TipTap Link openOnClick: false; registers a capturing click listener on editor root to intercept a[href] clicks, prevent navigation, focus editor, and open the link editor.
Toolbar link flow & URL handling
packages/ui/src/rich-text-area/rich-text-toolbar.tsx
Adds normalizeLinkUrl(); replaces prompt-based link flow with modal-driven editor using context state, computes link range/text/href on open, focuses input, supports Ctrl/Cmd+K, and implements Save/Delete to apply/unset link marks.

Sequence Diagrams

sequenceDiagram
    participant User
    participant Markdown as Markdown Renderer
    participant Tooltip as LinkHoverTooltip
    participant Browser as Browser DOM

    User->>Markdown: hover over rendered <a href>
    Markdown->>Browser: provide anchor element and href
    Tooltip->>Browser: measure anchor DOMRect
    Tooltip->>Tooltip: compute tooltip position
    Tooltip->>Browser: create portal and render URL tooltip
    User->>Markdown: mouseout
    Tooltip->>Browser: remove/hide tooltip
Loading
sequenceDiagram
    participant User
    participant Toolbar as RichText Toolbar
    participant Context as RichTextContext
    participant Editor as TipTap Editor
    participant Modal as Link Editor Modal

    User->>Toolbar: click link button or press Ctrl/Cmd+K
    Toolbar->>Context: setLinkEditorOpen(true)
    Context->>Modal: effect computes range/text/href and opens modal
    User->>Modal: Save or Delete
    Modal->>Editor: normalize URL and apply/unset link mark
    Modal->>Context: setLinkEditorOpen(false)
    Context->>Editor: resume editing
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • steven-tey

Poem

🐇 I nibble links with careful paws,
I measure, hover, mind the laws.
A tiny tooltip lifts the veil,
URLs gleam on a carrot trail.
Hop in — the editor hums and draws.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Messaging improvements" is vague and generic, using non-descriptive language that fails to convey the specific, substantial changes in this PR. Use a more specific title that captures the key feature, such as "Add link hover tooltips and link editor modal to rich text messages" or "Improve link handling in message composition and display".
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch messaging-composing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@marcusljf marcusljf marked this pull request as ready for review April 10, 2026 23:30
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/ui/src/rich-text-area/rich-text-toolbar.tsx (1)

163-182: Consider: Canceling the prompt removes an existing link.

When the user clicks Cancel on the prompt (returning null), the current logic calls unsetLink(), which removes an existing link. This may be unexpected if the user simply wanted to dismiss the dialog without making changes.

If preserving the link on cancel is desired, you could check for url === null (cancel) vs url === "" (cleared input):

💡 Optional fix to preserve link on cancel
  useEffect(() => {
    if (!editor || !linkEditorOpen) return;

    const previousUrl = editor.getAttributes("link").href;
    const url = window.prompt("Link URL", previousUrl);
+
+   // User cancelled the prompt
+   if (url === null) {
+     setLinkEditorOpen(false);
+     return;
+   }
+
    const normalizedUrl = url ? normalizeLinkUrl(url) : url;

-   if (!normalizedUrl?.trim()) {
+   if (!normalizedUrl.trim()) {
      editor.chain().focus().extendMarkRange("link").unsetLink().run();
    } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/rich-text-area/rich-text-toolbar.tsx` around lines 163 - 182,
The prompt cancel logic currently treats a null result the same as an empty
string and calls editor.chain().focus().extendMarkRange("link").unsetLink(),
removing existing links; modify the effect handling for linkEditorOpen so it
distinguishes url === null (user canceled) from url === "" (user cleared input).
Specifically, in the useEffect that reads previousUrl via
editor.getAttributes("link").href and calls normalizeLinkUrl, check for url ===
null and simply close the editor (setLinkEditorOpen(false)) without calling
unsetLink or setLink; only call unsetLink when url === "" or normalizedUrl is an
empty/whitespace string, and call setLink({ href: normalizedUrl }) when a valid
normalizedUrl exists. Ensure you reference editor, normalizeLinkUrl, unsetLink,
setLink, and linkEditorOpen in the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ui/src/rich-text-area/index.tsx`:
- Around line 160-184: The effect creates an infinite loop because it depends on
tooltipState and calls setTooltipState with a fresh rect object from updateRect;
fix by changing the dependency array to [href, !!tooltipState] (boolean presence
of tooltipState) instead of tooltipState, and remove the immediate call to
updateRect() inside the effect (rely on the existing onMouseEnter/onMouseLeave
logic to set tooltipState initially); keep updateRect, containerRef,
setTooltipState and the resize/scroll listeners and their cleanup as-is.

---

Nitpick comments:
In `@packages/ui/src/rich-text-area/rich-text-toolbar.tsx`:
- Around line 163-182: The prompt cancel logic currently treats a null result
the same as an empty string and calls
editor.chain().focus().extendMarkRange("link").unsetLink(), removing existing
links; modify the effect handling for linkEditorOpen so it distinguishes url ===
null (user canceled) from url === "" (user cleared input). Specifically, in the
useEffect that reads previousUrl via editor.getAttributes("link").href and calls
normalizeLinkUrl, check for url === null and simply close the editor
(setLinkEditorOpen(false)) without calling unsetLink or setLink; only call
unsetLink when url === "" or normalizedUrl is an empty/whitespace string, and
call setLink({ href: normalizedUrl }) when a valid normalizedUrl exists. Ensure
you reference editor, normalizeLinkUrl, unsetLink, setLink, and linkEditorOpen
in the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 00c99faf-175c-427a-b342-a8f0425c44ff

📥 Commits

Reviewing files that changed from the base of the PR and between e70888a and 086c172.

📒 Files selected for processing (4)
  • apps/web/ui/messages/message-markdown.tsx
  • packages/ui/src/rich-text-area/index.tsx
  • packages/ui/src/rich-text-area/rich-text-provider.tsx
  • packages/ui/src/rich-text-area/rich-text-toolbar.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ui/src/rich-text-area/rich-text-toolbar.tsx`:
- Around line 358-391: The label elements for the "Text" and "Link" fields are
not associated with their Input components (so screen readers won't announce
them); fix by adding matching id/htmlFor pairs or by wrapping each <Input>
inside its <label> for the Text and Link blocks — for example, give the Text
Input an id (e.g., text-input-unique) and set the label's htmlFor to that id,
and likewise give the Link Input an id (ensure it doesn't break linkInputRef
usage) and set its label's htmlFor to that id; keep existing handlers (onChange
using textValue/urlValue, onKeyDown calling saveLink) unchanged.
- Around line 69-78: The loop that expands the selection using linkMark (the
while blocks that adjust from/to and call state.doc.rangeHasMark) must also
verify the neighboring link mark's attributes match the current link's attrs
(e.g., href) before extending; update both loops to fetch the mark instance at
the adjacent position (via the doc resolve/node marks APIs) and compare its
attrs.href (or full attrs) to the original link mark's attrs and stop expanding
if they differ so back-to-back links with different hrefs are not merged into
one editable range.
- Around line 319-334: The current sequence always calls insertContentAt with
nextText which replaces the selected slice and strips inline marks; change the
logic in the toolbar handler so that before calling editor.chain().focus() you
compare the existing selected text (from selectionState.from..to) to nextText
and only call insertContentAt when they differ; if they are identical, skip
insertContentAt and only set the text selection (using setTextSelection with
from: selectionState.from, to: selectionState.from + nextText.length) and then
call setLink({ href: normalizedUrl }) to apply the link without rewriting the
node content, ensuring inline marks are preserved (update the code paths around
insertContentAt, setTextSelection, and setLink to branch on the equality check).
- Around line 27-34: normalizeLinkUrl currently accepts any URI scheme via
/^[a-z][a-z0-9+.-]*:/i which allows dangerous schemes like javascript: and
data:; update normalizeLinkUrl to only allow a safe scheme allowlist (at minimum
"http", "https", "mailto"), keep existing trimming and protocol-relative
handling (the startsWith("//") -> "https:" case) and when a scheme is present
but not in the allowlist return an empty string (or otherwise treat as invalid)
instead of returning the original URL, and when no scheme is present continue to
prepend "https://" to form a safe URL; ensure this logic is implemented inside
normalizeLinkUrl so callers (e.g., toolbar link insertion) receive only
allowlisted schemes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 17ca9e38-2a6a-4393-b14d-f66a2bdb5df1

📥 Commits

Reviewing files that changed from the base of the PR and between e270e5e and 96ec7ec.

📒 Files selected for processing (2)
  • packages/ui/src/rich-text-area/rich-text-provider.tsx
  • packages/ui/src/rich-text-area/rich-text-toolbar.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ui/src/rich-text-area/rich-text-provider.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/ui/src/rich-text-area/rich-text-toolbar.tsx (2)

433-441: Consider using the Button component for consistency.

The "Delete link" action uses a plain <button> with inline styles while "Cancel" and "Save" use the Button component. Using Button with variant="secondary" or a destructive variant would ensure consistent styling and behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/rich-text-area/rich-text-toolbar.tsx` around lines 433 - 441,
Replace the plain <button> used for the "Delete link" action with the shared
Button component to keep styling and behavior consistent; locate the block gated
by selectionState.isLink and swap the element that calls deleteLink to use
Button (e.g., Button with variant="secondary" or a destructive variant/prop your
Button supports), preserving the onClick handler, type="button", and
aria/labeling semantics so the functionality and accessibility remain unchanged.

242-243: Use useId() to avoid ID collisions with multiple editor instances.

These static IDs will conflict if multiple RichTextToolbar components are rendered on the same page, potentially causing accessibility issues where clicking a label focuses the wrong input.

Proposed fix using React's useId hook
+ import { useId } from "react";
  // ... in LinkButton component:
- const textInputId = "rich-text-link-text-input";
- const linkInputId = "rich-text-link-url-input";
+ const id = useId();
+ const textInputId = `${id}-text`;
+ const linkInputId = `${id}-url`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/rich-text-area/rich-text-toolbar.tsx` around lines 242 - 243,
The static IDs textInputId and linkInputId in the RichTextToolbar component
cause collisions for multiple instances; change the component to call React's
useId() (import useId from React) to generate a unique baseId inside the
RichTextToolbar function, then derive textInputId and linkInputId from that base
(e.g., `${baseId}-link-text` and `${baseId}-link-url`) and replace the hardcoded
strings wherever textInputId and linkInputId are used (labels, inputs, aria
attributes) to ensure each toolbar instance has unique, accessible IDs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/ui/src/rich-text-area/rich-text-toolbar.tsx`:
- Around line 433-441: Replace the plain <button> used for the "Delete link"
action with the shared Button component to keep styling and behavior consistent;
locate the block gated by selectionState.isLink and swap the element that calls
deleteLink to use Button (e.g., Button with variant="secondary" or a destructive
variant/prop your Button supports), preserving the onClick handler,
type="button", and aria/labeling semantics so the functionality and
accessibility remain unchanged.
- Around line 242-243: The static IDs textInputId and linkInputId in the
RichTextToolbar component cause collisions for multiple instances; change the
component to call React's useId() (import useId from React) to generate a unique
baseId inside the RichTextToolbar function, then derive textInputId and
linkInputId from that base (e.g., `${baseId}-link-text` and
`${baseId}-link-url`) and replace the hardcoded strings wherever textInputId and
linkInputId are used (labels, inputs, aria attributes) to ensure each toolbar
instance has unique, accessible IDs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 47b8e856-5565-4c13-b373-42770b2344c3

📥 Commits

Reviewing files that changed from the base of the PR and between 96ec7ec and 635c55c.

📒 Files selected for processing (1)
  • packages/ui/src/rich-text-area/rich-text-toolbar.tsx

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