Skip to content

Support sending gif and text#2018

Closed
simo6529 wants to merge 5 commits intomainfrom
support-sending-gif-and-text
Closed

Support sending gif and text#2018
simo6529 wants to merge 5 commits intomainfrom
support-sending-gif-and-text

Conversation

@simo6529
Copy link
Copy Markdown
Collaborator

@simo6529 simo6529 commented Feb 27, 2026

Summary by CodeRabbit

  • New Features

    • Editor converts standalone image URLs into preview images and exposes imperative methods to insert text or image previews via the input component.
    • URL-preview extraction added for embedding preview-only images in exports.
  • Improvements

    • Smart-spacing when inserting text or images to preserve surrounding whitespace.
    • Tenor GIF previews render with a consistent fixed height; GIF selection inserts a preview rather than auto-submitting.
  • Tests

    • Extensive unit tests added/expanded for image URL handling, GIF behavior, insertion logic, and related plugins.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

This PR adds a URL-preview system for the Lexical editor: new InsertTextPlugin (imperative insertion with smart spacing), StandaloneImageUrlPreviewPlugin (auto-convert standalone image URLs on space/paste), a URL preview transformer/constants, centralized GIF utilities, ImageNode/Component updates for preview rendering and loading fallback, and integration + tests across CreateDropInput/CreateDropContent.

Changes

Cohort / File(s) Summary
Lexical Plugins
components/drops/create/lexical/plugins/InsertTextPlugin.tsx, components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.tsx
New plugins: InsertTextPlugin exposes imperative handles to insert text and image previews (smart spacing, image-node creation); StandaloneImageUrlPreviewPlugin converts standalone image URLs to image nodes on space/paste with token parsing and cursor handling.
Transformer & Constants
components/drops/create/lexical/transformers/UrlPreviewImageTransformer.ts, components/drops/create/lexical/nodes/urlPreviewImage.constants.ts
New markdown transformer that exports the source URL for preview images identified by a sentinel alt text; new exported constant URL_PREVIEW_IMAGE_ALT_TEXT.
GIF Utilities & Rendering
components/waves/drops/gifPreview.ts, components/drops/view/part/dropPartMarkdown/handlers/gif.tsx, components/drops/view/part/dropPartMarkdown/renderers.tsx
Centralized GIF utilities (CHAT_GIF_PREVIEW_HEIGHT_PX, isTenorGifUrl) and migration of Tenor detection/height usage to this module; renderer uses centralized height.
Image Node / Component
components/drops/create/lexical/nodes/ImageNode.tsx, components/drops/create/lexical/nodes/ImageComponent.tsx
ImageNode: adds LOADING_IMAGE_SRC fallback and uses GIF preview height for placeholder; ImageComponent: uses URL preview alt-text detection and applies fixed preview height / width:auto for Tenor GIFs.
Editor Integration
components/waves/CreateDropInput.tsx, components/waves/CreateDropContent.tsx
CreateDropInput: exposes new forwarded ref methods (insertTextAtCursor, insertImagePreviewFromUrl) and renders plugins; CreateDropContent: integrates transformer and refactors GIF handling to insert a preview URL instead of creating a GIF drop.
Tests
__tests__/components/.../ImageComponent.test.tsx, .../ImageNode.test.tsx, .../plugins/InsertTextPlugin.test.tsx, .../plugins/StandaloneImageUrlPreviewPlugin.test.tsx, .../transformers/UrlPreviewImageTransformer.test.ts, __tests__/components/waves/CreateDropContent.gifInsert.test.tsx, __tests__/components/waves/CreateDropInput.test.tsx
Extensive test additions/updates covering plugin behaviors, smart spacing, token conversion on space/paste, transformer export logic, GIF preview styling/height, loading fallback, and ref API exposure.

Sequence Diagram

sequenceDiagram
    actor User
    participant Editor as Lexical Editor
    participant StandalonePlugin as StandaloneImageUrlPreviewPlugin
    participant InsertTextPlugin as InsertTextPlugin
    participant ImageNode as Image Node
    participant Transformer as UrlPreviewImageTransformer

    User->>Editor: Type or paste an image URL
    Editor->>StandalonePlugin: KEY_SPACE / PASTE event with token
    StandalonePlugin->>StandalonePlugin: Parse token, validate image URL
    StandalonePlugin->>ImageNode: Create image node (alt = URL_PREVIEW_IMAGE_ALT_TEXT)
    StandalonePlugin->>Editor: Replace token with image node, adjust cursor
    User->>Editor: Trigger InsertTextPlugin via ref (insertImagePreviewFromUrl)
    InsertTextPlugin->>Editor: Insert image node with smart spacing
    User->>Editor: Request markdown export
    Editor->>Transformer: Inspect nodes
    Transformer->>ImageNode: If alt == URL_PREVIEW_IMAGE_ALT_TEXT -> return src
    Transformer-->>Editor: Export URL in markdown
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • ragnep
  • prxt6529

Poem

🐰 I hop and tuck a URL in place,
Smart spaces stitch text and GIF with grace.
Tenor previews stand tall at fixed height,
Nodes transform to markdown when it's right.
Hooray — the editor hums, the rabbit grins. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Support sending gif and text' clearly summarizes the main changes in this PR, which add functionality to insert GIFs and text into the editor via new plugins and utilities.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch support-sending-gif-and-text

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.

Copy link
Copy Markdown

@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: 2

🧹 Nitpick comments (3)
components/drops/create/lexical/plugins/InsertTextPlugin.tsx (1)

156-156: Consider using null return instead of empty fragment.

Returning null is slightly more idiomatic for components that render nothing, as it avoids creating an empty React fragment node.

💡 Minor suggestion
-  return <></>;
+  return null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drops/create/lexical/plugins/InsertTextPlugin.tsx` at line 156, In
the InsertTextPlugin React component (InsertTextPlugin), replace the empty
fragment return with returning null to avoid creating an unnecessary React node;
locate the return statement that currently yields <> </> and change it so the
component returns null when it should render nothing.
components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.tsx (1)

53-65: Consider using a more accurate type annotation.

The textNode parameter type ReturnType<typeof $createTextNode> is slightly misleading since this function receives an existing text node from the editor, not a newly created one. While functionally correct (both are TextNode), a more explicit type would improve clarity.

💡 Suggested type improvement
+import type { TextNode } from "lexical";
+
 const replaceTokenWithPreviewNode = ({
   textNode,
   start,
   end,
   url,
   appendTrailingSpace,
 }: {
-  readonly textNode: ReturnType<typeof $createTextNode>;
+  readonly textNode: TextNode;
   readonly start: number;
   readonly end: number;
   readonly url: string;
   readonly appendTrailingSpace: boolean;
 }): void => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.tsx`
around lines 53 - 65, The parameter type for textNode in
replaceTokenWithPreviewNode is misleadingly declared as ReturnType<typeof
$createTextNode>; change it to the explicit TextNode type (imported as a type
from 'lexical') and update the function signature accordingly, replacing
ReturnType<typeof $createTextNode> with TextNode and adding the appropriate
type-only import to the file so the annotation clearly reflects that the
function receives an existing editor TextNode.
__tests__/components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.test.tsx (1)

62-156: Good test coverage, consider adding Shift+Enter test.

The tests cover space key and paste interactions well. Consider adding a test for the Shift+Enter behavior to ensure complete coverage of the plugin's command handlers.

💡 Suggested additional test case
it("converts standalone image URL token on shift+enter without preventing default", () => {
  const commandHandlers = new Map<string, (event?: any) => boolean>();
  const editor = {
    registerCommand: jest.fn(
      (command: string, handler: (event?: any) => boolean) => {
        commandHandlers.set(command, handler);
        return () => {};
      }
    ),
    update: (fn: () => void) => fn(),
  };
  useLexicalComposerContextMock.mockReturnValue([editor]);

  const urlTextNode = {
    __isTextNode: true as const,
    getTextContent: () => "https://example.com/cat.gif",
    replace: jest.fn(),
  };
  getSelectionMock.mockReturnValue({
    isCollapsed: () => true,
    anchor: {
      getNode: () => urlTextNode,
      offset: "https://example.com/cat.gif".length,
    },
  });

  render(<StandaloneImageUrlPreviewPlugin />);

  const handled = commandHandlers.get("KEY_ENTER_COMMAND")?.({
    shiftKey: true,
    preventDefault: jest.fn(),
  });

  // Returns false to allow normal enter behavior
  expect(handled).toBe(false);
  expect(urlTextNode.replace).toHaveBeenCalled();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@__tests__/components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.test.tsx`
around lines 62 - 156, Add a test for Shift+Enter behavior in
StandaloneImageUrlPreviewPlugin: register the KEY_ENTER_COMMAND handler into
commandHandlers (like other tests), mock a collapsed selection returning a
urlTextNode (with getTextContent and replace), render
StandaloneImageUrlPreviewPlugin, invoke the KEY_ENTER_COMMAND handler with an
event containing shiftKey: true and a preventDefault mock, and assert the
handler returns false (so default enter is allowed) and that urlTextNode.replace
was still called; ensure you reference the same helpers/mocks used elsewhere
(useLexicalComposerContextMock, getSelectionMock, commandHandlers map,
urlTextNode.replace).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/drops/create/lexical/nodes/ImageComponent.tsx`:
- Around line 26-27: The code currently passes the sentinel
URL_PREVIEW_IMAGE_ALT_TEXT through to the DOM via alt (and variable
isUrlPreviewGif relies on it); update ImageComponent so the internal sentinel
value (URL_PREVIEW_IMAGE_ALT_TEXT) is never rendered to the alt attribute—map
that sentinel to a human-friendly alt or an empty string (e.g., alt="" for
decorative previews) before assigning to the DOM, while keeping isUrlPreviewGif,
altText and src logic intact; ensure any special handling for GIF previews still
uses the mapped/display alt and not the sentinel value.

In `@components/drops/create/lexical/transformers/UrlPreviewImageTransformer.ts`:
- Around line 19-20: The regExp /(?:)/ in UrlPreviewImageTransformer is a
zero-length pattern that matches anywhere and the replace: () => {} is a
dangerous no-op; change regExp to a non-matching pattern (e.g. /$^/) and make
replace a safe identity function (e.g. replace: (match) => match) so the
transformer’s regExp and replace fields are defensive and won’t accidentally
alter input if the transformer is later used in import flows.

---

Nitpick comments:
In
`@__tests__/components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.test.tsx`:
- Around line 62-156: Add a test for Shift+Enter behavior in
StandaloneImageUrlPreviewPlugin: register the KEY_ENTER_COMMAND handler into
commandHandlers (like other tests), mock a collapsed selection returning a
urlTextNode (with getTextContent and replace), render
StandaloneImageUrlPreviewPlugin, invoke the KEY_ENTER_COMMAND handler with an
event containing shiftKey: true and a preventDefault mock, and assert the
handler returns false (so default enter is allowed) and that urlTextNode.replace
was still called; ensure you reference the same helpers/mocks used elsewhere
(useLexicalComposerContextMock, getSelectionMock, commandHandlers map,
urlTextNode.replace).

In `@components/drops/create/lexical/plugins/InsertTextPlugin.tsx`:
- Line 156: In the InsertTextPlugin React component (InsertTextPlugin), replace
the empty fragment return with returning null to avoid creating an unnecessary
React node; locate the return statement that currently yields <> </> and change
it so the component returns null when it should render nothing.

In `@components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.tsx`:
- Around line 53-65: The parameter type for textNode in
replaceTokenWithPreviewNode is misleadingly declared as ReturnType<typeof
$createTextNode>; change it to the explicit TextNode type (imported as a type
from 'lexical') and update the function signature accordingly, replacing
ReturnType<typeof $createTextNode> with TextNode and adding the appropriate
type-only import to the file so the annotation clearly reflects that the
function receives an existing editor TextNode.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f442271 and 3e9a47d.

📒 Files selected for processing (16)
  • __tests__/components/drops/create/lexical/nodes/ImageComponent.test.tsx
  • __tests__/components/drops/create/lexical/plugins/InsertTextPlugin.test.tsx
  • __tests__/components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.test.tsx
  • __tests__/components/drops/create/lexical/transformers/UrlPreviewImageTransformer.test.ts
  • __tests__/components/waves/CreateDropContent.gifInsert.test.tsx
  • __tests__/components/waves/CreateDropInput.test.tsx
  • components/drops/create/lexical/nodes/ImageComponent.tsx
  • components/drops/create/lexical/nodes/urlPreviewImage.constants.ts
  • components/drops/create/lexical/plugins/InsertTextPlugin.tsx
  • components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.tsx
  • components/drops/create/lexical/transformers/UrlPreviewImageTransformer.ts
  • components/drops/view/part/dropPartMarkdown/handlers/gif.tsx
  • components/drops/view/part/dropPartMarkdown/renderers.tsx
  • components/waves/CreateDropContent.tsx
  • components/waves/CreateDropInput.tsx
  • components/waves/drops/gifPreview.ts

Comment thread components/drops/create/lexical/nodes/ImageComponent.tsx
Comment thread components/drops/create/lexical/transformers/UrlPreviewImageTransformer.ts Outdated
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Mar 2, 2026

Copy link
Copy Markdown

@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

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

Inline comments:
In `@components/drops/create/lexical/plugins/InsertTextPlugin.tsx`:
- Around line 14-15: The plugin currently uses the generic sentinel string
"loading" which can collide with real image src values; define a shared,
namespaced constant (for example __URL_PREVIEW_LOADING__) in a central module
and replace all hard-coded "loading" usages in InsertTextPlugin
(InsertTextPlugin.tsx), the nodes ImageNode (ImageNode.tsx) and ImageComponent
(ImageComponent.tsx), and any other occurrences (the other referenced locations)
to import and use this constant instead so they all reference the same
non-colliding sentinel.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e9a47d and e1c57c8.

📒 Files selected for processing (9)
  • __tests__/components/drops/create/lexical/nodes/ImageComponent.test.tsx
  • __tests__/components/drops/create/lexical/nodes/ImageNode.test.tsx
  • __tests__/components/drops/create/lexical/plugins/InsertTextPlugin.test.tsx
  • __tests__/components/drops/create/lexical/transformers/UrlPreviewImageTransformer.test.ts
  • components/drops/create/lexical/nodes/ImageComponent.tsx
  • components/drops/create/lexical/nodes/ImageNode.tsx
  • components/drops/create/lexical/plugins/InsertTextPlugin.tsx
  • components/drops/create/lexical/transformers/UrlPreviewImageTransformer.ts
  • components/waves/CreateDropContent.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/components/drops/create/lexical/transformers/UrlPreviewImageTransformer.test.ts
  • components/waves/CreateDropContent.tsx
  • components/drops/create/lexical/transformers/UrlPreviewImageTransformer.ts

Comment on lines +14 to +15
import { URL_PREVIEW_IMAGE_ALT_TEXT } from "../nodes/urlPreviewImage.constants";
import { isTenorGifUrl } from "@/components/waves/drops/gifPreview";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use a non-colliding shared loading sentinel.

"loading" is too generic for a sentinel and can collide with legitimate src values. Please move to a shared, namespaced constant (e.g. __URL_PREVIEW_LOADING__) and reuse it across plugin/node/component paths.

🔧 Proposed fix
-import { URL_PREVIEW_IMAGE_ALT_TEXT } from "../nodes/urlPreviewImage.constants";
+import {
+  URL_PREVIEW_IMAGE_ALT_TEXT,
+  URL_PREVIEW_LOADING_SRC,
+} from "../nodes/urlPreviewImage.constants";
@@
-const LOADING_IMAGE_SRC = "loading";
@@
-        src: shouldUseLoadingPlaceholder ? LOADING_IMAGE_SRC : normalizedUrl,
+        src: shouldUseLoadingPlaceholder
+          ? URL_PREVIEW_LOADING_SRC
+          : normalizedUrl,
@@
-        if (placeholderNode.getSrc() !== LOADING_IMAGE_SRC) {
+        if (placeholderNode.getSrc() !== URL_PREVIEW_LOADING_SRC) {
           return;
         }

Also update components/drops/create/lexical/nodes/ImageNode.tsx and components/drops/create/lexical/nodes/ImageComponent.tsx to consume the same shared constant.

Also applies to: 28-29, 138-138, 170-170

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

In `@components/drops/create/lexical/plugins/InsertTextPlugin.tsx` around lines 14
- 15, The plugin currently uses the generic sentinel string "loading" which can
collide with real image src values; define a shared, namespaced constant (for
example __URL_PREVIEW_LOADING__) in a central module and replace all hard-coded
"loading" usages in InsertTextPlugin (InsertTextPlugin.tsx), the nodes ImageNode
(ImageNode.tsx) and ImageComponent (ImageComponent.tsx), and any other
occurrences (the other referenced locations) to import and use this constant
instead so they all reference the same non-colliding sentinel.

@simo6529 simo6529 closed this Mar 2, 2026
@simo6529 simo6529 deleted the support-sending-gif-and-text branch March 2, 2026 15:17
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