Skip to content

Wire up quote posts in web-next#211

Open
malkoG wants to merge 2 commits intohackers-pub:mainfrom
malkoG:feature/quoting-on-web-next
Open

Wire up quote posts in web-next#211
malkoG wants to merge 2 commits intohackers-pub:mainfrom
malkoG:feature/quoting-on-web-next

Conversation

@malkoG
Copy link
Contributor

@malkoG malkoG commented Mar 1, 2026

Summary

  • Wire up the quote button in PostControls to open the note composer modal with the quoted post's ID
  • Add quotedPostId signal to NoteComposeContext so the compose flow tracks which post is being quoted
  • Fetch and display a compact quoted post preview (avatar, name, handle, content snippet) in NoteComposer, with a dismiss button to remove the quote
  • Pass quotedPostId through to the createNote mutation input

Screenshot

스크린샷 2026-03-01 18 20 35

Test plan

  • Click the quote button on a note — modal opens with "Quote" title and quoted post preview
  • Verify the preview shows the author's avatar, name, handle, and content snippet
  • Click the X button on the preview — quote is removed, title changes to "Create Note"
  • Submit a quote note — note is created with the quote association
  • Open the compose modal from the sidebar — no quote preview, title shows "Create Note"

Summary by CodeRabbit

  • New Features

    • Quoted-post preview added to the composer with a remove-quote action.
    • Quote button opens the composer pre-filled with the selected post.
    • Dialog title displays "Quote" when composing a quote.
  • Behavior

    • Composer still closes automatically after successful submission.
  • Localization

    • Added "Remove quote" translation and UI label.

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds quote support to the note composer: PostControls opens the composer with a quoted post id; NoteComposeContext stores/clears quotedPostId and exposes open(quotedPostId) and clearQuote; NoteComposeModal and NoteComposer accept/render quoted-post data and allow removing the quote; minor AppSidebar onClick adjustment.

Changes

Cohort / File(s) Summary
Context State Management
web-next/src/contexts/NoteComposeContext.tsx
Adds quotedPostId signal/accessor, changes open to accept optional quotedPostId, adds clearQuote(), clears quote on close; context value expanded.
Composer & Modal
web-next/src/components/NoteComposer.tsx, web-next/src/components/NoteComposeModal.tsx
NoteComposer props extended (quotedPostId?, onQuoteRemoved?); fetches quoted-post preview via Relay query, renders preview block with avatar/name/content and removal control; includes quotedPostId in create mutation. NoteComposeModal reads quotedPostId, toggles title ("Quote" vs "Create Note") and forwards onQuoteRemoved.
Controls & Integration
web-next/src/components/PostControls.tsx, web-next/src/components/AppSidebar.tsx
PostControls wired to call useNoteCompose().open(quotedPostId) from Quote button; AppSidebar Compose handler changed to use inline arrow calling compose opener.
Localization
web-next/src/locales/*/messages.po
Adds translation key "Remove quote" across locales and updates source reference anchors for NoteComposer/NoteComposeModal/PostControls strings.

Sequence Diagram

sequenceDiagram
    actor User
    participant PostControls as "PostControls"
    participant NoteComposeContext as "NoteComposeContext"
    participant NoteComposeModal as "NoteComposeModal"
    participant NoteComposer as "NoteComposer"
    participant Relay as "Relay (GraphQL)"

    User->>PostControls: Click "Quote" on a post
    PostControls->>NoteComposeContext: open(quotedPostId)
    NoteComposeContext->>NoteComposeContext: store quotedPostId, set isOpen
    NoteComposeContext->>NoteComposeModal: open modal (with quotedPostId)
    NoteComposeModal->>NoteComposer: render with quotedPostId & onQuoteRemoved
    NoteComposer->>Relay: fetch quoted-post preview (NoteComposerQuotedPostQuery)
    Relay-->>NoteComposer: return preview data
    NoteComposer->>User: render quoted preview + editor
    User->>NoteComposer: Click "Remove quote"
    NoteComposer->>NoteComposeContext: clearQuote()
    NoteComposeContext-->>NoteComposer: quotedPostId cleared
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • dahlia

Poem

🐰 I nudged a quote into the stream,
A tiny preview, bright as cream,
Tap to add, tap to clear,
The composer hums — a joyful cheer! 🎉

🚥 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 'Wire up quote posts in web-next' accurately describes the main feature addition: implementing quote post functionality across multiple components in the web-next application.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces the functionality to quote existing posts within the application. It enables users to initiate a new note composition with a reference to another post, displaying a preview of the quoted content directly within the composer. This enhancement streamlines the process of responding to or referencing other posts, improving user interaction and content linking.

Highlights

  • Quote Button Integration: The quote button in PostControls has been wired up to open the note composer modal, pre-filling it with the ID of the post being quoted.
  • Quoted Post Context: A quotedPostId signal has been added to NoteComposeContext to allow the compose flow to track which post is being quoted, and functions to clear this ID have been introduced.
  • Quoted Post Preview: The NoteComposer now fetches and displays a compact preview of the quoted post, including the author's avatar, name, handle, and a content snippet. A dismiss button is available to remove the quote.
  • Mutation Update: The createNote mutation now accepts and passes the quotedPostId as part of its input, ensuring the quote association is persisted.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • web-next/src/components/AppSidebar.tsx
    • Modified the onClick handler for the SidebarMenuButton to explicitly call openNoteCompose() as a function.
  • web-next/src/components/NoteComposeModal.tsx
    • Updated the useNoteCompose hook to destructure quotedPostId and clearQuote.
    • Dynamically changed the DialogTitle to 'Quote' when a quotedPostId is present, otherwise it defaults to 'Create Note'.
    • Passed quotedPostId and onQuoteRemoved props to the NoteComposer component.
  • web-next/src/components/NoteComposer.tsx
    • Added new imports for fetchQuery, onCleanup, useRelayEnvironment, Avatar, AvatarImage, and IconX.
    • Defined a new GraphQL query NoteComposerQuotedPostQuery to fetch details for a quoted post.
    • Introduced QuotedPostPreview interface to type the fetched quoted post data.
    • Extended NoteComposerProps to include optional quotedPostId and onQuoteRemoved properties.
    • Implemented a createEffect to fetch and set quotedPost data when props.quotedPostId changes, with cleanup on unsubscription.
    • Added setQuotedPost(null) to the reset function to clear the quoted post state.
    • Included quotedPostId in the CreateNoteInput for the NoteComposerMutation.
    • Added a Show component to conditionally render a compact preview of the quotedPost, displaying actor details and content snippet, along with a dismiss button.
  • web-next/src/components/PostControls.tsx
    • Imported the useNoteCompose hook.
    • Destructured the open function from useNoteCompose.
    • Added an onClick handler to the quote button that calls openCompose with the current note's ID.
  • web-next/src/contexts/NoteComposeContext.tsx
    • Updated NoteComposeContextValue interface to include quotedPostId and clearQuote, and modified the open method to accept an optional quotedPostId.
    • Introduced a quotedPostId signal to manage the ID of the post being quoted.
    • Modified the open function to set the quotedPostId when opening the composer.
    • Modified the close function to clear the quotedPostId.
    • Added a clearQuote function to explicitly clear the quotedPostId.
    • Updated the NoteComposeContext.Provider value to include the new quotedPostId and clearQuote.
Activity
  • The pull request was created by malkoG to implement quote post functionality.
  • The author provided a detailed summary and test plan in the pull request description, outlining the changes and verification steps.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request wires up the quote post functionality in the new web-next stack. The changes correctly update the context, modal, and composer to handle quoting a post. The implementation looks good, but I've found a critical security vulnerability (XSS), a potential runtime error, and some areas for improving robustness in data fetching that should be addressed.

Comment on lines +246 to +249
<div
class="text-sm text-muted-foreground mt-1 line-clamp-3 prose prose-sm dark:prose-invert max-w-none"
innerHTML={html()}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

Using innerHTML with content that can come from external federated sources (contentHtml) is a significant security risk, as it can lead to Cross-Site Scripting (XSS) attacks. Malicious HTML or script tags in the content could be executed in the user's browser.

To mitigate this, you must sanitize the HTML content before rendering it. A popular library for this is DOMPurify.

Copy link
Contributor Author

@malkoG malkoG Mar 1, 2026

Choose a reason for hiding this comment

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

This is consistent with the rest of the codebase — all HTML content is sanitized server-side via sanitizeHtml() using the xss library (FilterXSS with an allowlist) before it's stored or served through GraphQL.
The preprocessContentHtml() function calls sanitizeHtml() as its first step for all content.

References:

hackerspub/models/html.ts

Lines 296 to 298 in 1463561

export function sanitizeHtml(html: string): string {
return htmlXss.process(html);
}

hackerspub/models/html.ts

Lines 423 to 432 in 1463561

export function preprocessContentHtml(
html: string,
{ mentions, tags, emojis = {}, quote }: PreprocessContentHtmlOptions,
) {
html = sanitizeHtml(html);
html = transformMentions(html, mentions, tags);
html = renderCustomEmojis(html, emojis);
if (quote) html = transformMisskeyInlineQuote(html);
return html;
}

const htmlXss = new FilterXSS({
allowList: {
a: [
"lang",
"translate",
"target",
"href",
"hreflang",
"title",
"rel",
"class",
"data-username",
"data-host",
"data-id",
"data-iri",
"data-internal-href",
"id",
],
abbr: ["lang", "translate", "title"],
address: ["lang", "translate"],
area: ["lang", "translate", "shape", "coords", "href", "alt"],
article: ["lang", "translate"],
aside: ["lang", "translate"],
audio: [
"lang",
"translate",
"autoplay",
"controls",
"crossorigin",
"loop",
"muted",
"preload",
"src",
],
b: ["lang", "translate"],
bdi: ["lang", "translate", "dir"],
bdo: ["lang", "translate", "dir"],
big: ["lang", "translate"],
blockquote: ["lang", "translate", "cite"],
br: ["lang", "translate"],
caption: ["lang", "translate"],
center: ["lang", "translate"],
cite: ["lang", "translate"],
code: ["lang", "translate"],
col: ["lang", "translate", "align", "valign", "span", "width"],
colgroup: ["lang", "translate", "align", "valign", "span", "width"],
dd: ["lang", "translate"],
del: ["lang", "translate", "datetime"],
details: ["lang", "translate", "open"],
dfn: ["lang", "translate"],
div: ["lang", "translate", "class", "style"],
dl: ["lang", "translate"],
dt: ["lang", "translate"],
em: ["lang", "translate"],
figcaption: ["lang", "translate"],
figure: ["lang", "translate"],
font: ["lang", "translate", "color", "size", "face"],
footer: ["lang", "translate"],
h1: ["lang", "translate", "id"],
h2: ["lang", "translate", "id"],
h3: ["lang", "translate", "id"],
h4: ["lang", "translate", "id"],
h5: ["lang", "translate", "id"],
h6: ["lang", "translate", "id"],
header: ["lang", "translate"],
hr: ["class"],
i: ["lang", "translate"],
img: [
"lang",
"translate",
"src",
"alt",
"title",
"width",
"height",
"loading",
],
ins: ["lang", "translate", "datetime"],
kbd: ["lang", "translate"],
li: ["class", "id", "lang", "translate"],
mark: ["lang", "translate"],
nav: ["lang", "translate"],
ol: ["class", "lang", "translate", "start"],
p: ["class", "dir", "lang", "translate"],
picture: ["lang", "translate"],
pre: ["lang", "translate", "class", "style"],
q: ["lang", "translate", "cite"],
rp: ["lang", "translate"],
rt: ["lang", "translate"],
ruby: ["lang", "translate"],
s: ["lang", "translate"],
samp: ["lang", "translate"],
section: ["lang", "translate", "class"],
small: ["lang", "translate"],
source: [
"lang",
"translate",
"src",
"srcset",
"sizes",
"media",
"type",
"width",
"height",
],
span: ["lang", "translate", "aria-hidden", "class", "style"],
sub: ["lang", "translate"],
summary: ["lang", "translate"],
sup: ["class", "lang", "translate", "class"],
strong: ["lang", "translate"],
strike: ["lang", "translate"],
table: ["lang", "translate", "width", "border", "align", "valign"],
tbody: ["lang", "translate", "align", "valign"],
td: ["lang", "translate", "width", "rowspan", "colspan", "align", "valign"],
tfoot: ["lang", "translate", "align", "valign"],
th: ["lang", "translate", "width", "rowspan", "colspan", "align", "valign"],
thead: ["lang", "translate", "align", "valign"],
time: ["lang", "translate", "datetime"],
tr: ["lang", "translate", "rowspan", "align", "valign"],
tt: ["lang", "translate"],
u: ["lang", "translate"],
ul: ["lang", "translate"],
var: ["lang", "translate"],
video: [
"lang",
"translate",
"autoplay",
"controls",
"crossorigin",
"loop",
"muted",
"playsinline",
"poster",
"preload",
"src",
"height",
"width",
],
// MathML
math: ["class", "xmlns"],
maction: ["actiontype", "selection"],
annotation: ["encoding"],
"annotation-xml": ["encoding"],
menclose: ["notation"],
merror: ["class"],
mfenced: ["open", "close", "separators"],
mfrac: ["linethickness"],
mi: ["mathvariant"],
mmultiscripts: ["subscriptshift", "superscriptshift"],
mn: ["mathvariant"],
mo: ["fence", "lspace", "rspace", "stretchy"],
mover: ["accent"],
mpadded: ["height", "depth", "width", "lspace", "voffset"],
mphantom: ["class"],
mprescripts: [],
mroot: ["displaystyle"],
mrow: ["displaystyle"],
ms: ["lquote", "rquote"],
semantics: ["class"],
mspace: ["depth", "height", "width"],
msqrt: ["displaystyle"],
mstyle: ["displaystyle", "mathcolor", "mathbackground"],
msub: ["subscriptshift"],
msup: ["superscriptshift"],
msubsup: ["subscriptshift", "superscriptshift"],
mtable: [
"align",
"columnalign",
"columnspacing",
"columnlines",
"rowalign",
"rowspacing",
"rowlines",
],
mtd: ["columnalign", "rowalign"],
mtext: ["mathvariant"],
mtr: ["columnalign", "rowalign"],
munder: ["accentunder"],
munderover: ["accent", "accentunder"],
eq: [],
eqn: [],
// SVG
svg: ["class", "viewBox", "version", "width", "height", "aria-hidden"],
path: [
"d",
"fill",
"fill-opacity",
"stroke",
"stroke-width",
"stroke-opacity",
"stroke-dasharray",
],
g: [
"id",
"class",
"fill",
"fill-opacity",
"stroke",
"stroke-width",
"stroke-opacity",
"stroke-dasharray",
"transform",
],
polygon: [
"points",
"fill",
"fill-opacity",
"stroke",
"stroke-width",
"stroke-opacity",
"stroke-dasharray",
],
polyline: [
"points",
"fill",
"fill-opacity",
"stroke",
"stroke-width",
"stroke-opacity",
"stroke-dasharray",
],
ellipse: [
"cx",
"cy",
"rx",
"ry",
"fill",
"fill-opacity",
"stroke",
"stroke-width",
"stroke-opacity",
"stroke-dasharray",
],
linearGradient: ["id", "gradientUnits", "x1", "y1", "x2", "y2"],
radialGradient: ["id", "gradientUnits", "cx", "cy", "r", "fx", "fy"],
stop: ["offset", "stop-color", "stop-opacity", "style"],
text: [
"x",
"y",
"fill",
"fill-opacity",
"font-size",
"font-family",
"font-weight",
"font-style",
"text-anchor",
],
defs: [],
title: [],
},
css: {
whiteList: {
...cssfilter.whiteList,
color: true,
"background-color": true,
"font-style": true,
"font-weight": true,
"text-decoration": true,
// Shiki
"--shiki-dark": true,
"--shiki-dark-bg": true,
"--shiki-dark-font-style": true,
"--shiki-dark-font-weight": true,
"--shiki-dark-text-decoration": true,
// SVG
"stop-color": true,
"stop-opacity": true,
},
},
});

Comment on lines +123 to +130
setQuotedPost({
typename: node.__typename,
contentHtml: node.content,
name: "name" in node ? (node.name ?? undefined) : undefined,
actorName: node.actor?.name ?? undefined,
actorHandle: node.actor.handle,
actorAvatarUrl: node.actor.avatarUrl,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

high

There's an inconsistency in how node.actor properties are accessed. node.actor?.name uses optional chaining, suggesting actor could be null, but node.actor.handle and node.actor.avatarUrl are accessed directly. If node.actor is ever null, this will cause a runtime crash.

Please add a guard to handle cases where actor might be missing, or use optional chaining consistently if the downstream components can handle undefined values (which they currently can't, as actorHandle and actorAvatarUrl are required in QuotedPostPreview).

          if (!node.actor) {
            setQuotedPost(null);
            showToast({
              title: t`Error`,
              description: t`Could not load quoted post author.`,
              variant: "error",
            });
            return;
          }
          setQuotedPost({
            typename: node.__typename,
            contentHtml: node.content,
            name: "name" in node ? (node.name ?? undefined) : undefined,
            actorName: node.actor.name ?? undefined,
            actorHandle: node.actor.handle,
            actorAvatarUrl: node.actor.avatarUrl,
          });

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same with this #211 (comment)

Comment on lines +118 to +136
).subscribe({
next(data) {
const node = data.node;
if (!node) return;
if (node.__typename === "Note" || node.__typename === "Article") {
setQuotedPost({
typename: node.__typename,
contentHtml: node.content,
name: "name" in node ? (node.name ?? undefined) : undefined,
actorName: node.actor?.name ?? undefined,
actorHandle: node.actor.handle,
actorAvatarUrl: node.actor.avatarUrl,
});
}
},
error() {
setQuotedPost(null);
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The subscription logic for fetchQuery can be improved for better robustness and user experience:

  1. In the next handler, if data.node is null (e.g., post not found), the quotedPost state is not cleared. This can lead to showing stale data from a previous quote. The state should be cleared.
  2. In the error handler, the fetch failure is handled silently. It would be better to show a toast notification to inform the user that loading the quoted post failed.
    ).subscribe({
      next(data) {
        const node = data.node;
        if (!node) {
          setQuotedPost(null);
          return;
        }
        if (node.__typename === "Note" || node.__typename === "Article") {
          setQuotedPost({
            typename: node.__typename,
            contentHtml: node.content,
            name: "name" in node ? (node.name ?? undefined) : undefined,
            actorName: node.actor?.name ?? undefined,
            actorHandle: node.actor.handle,
            actorAvatarUrl: node.actor.avatarUrl,
          });
        }
      },
      error(err) {
        console.error("Failed to fetch quoted post:", err);
        showToast({
          title: t`Error`,
          description: t`Failed to load quoted post.`,
          variant: "error",
        });
        setQuotedPost(null);
      },
    });

Copy link

@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)
web-next/src/components/NoteComposer.tsx (1)

44-44: Rename the query constant to avoid redeclaration with the imported type.

The static analyzer correctly identifies that NoteComposerQuotedPostQuery is declared both as an imported type (line 22) and as a const (line 44). While this works because one is a type-only import, it can cause confusion and some tooling issues.

♻️ Suggested rename
-const NoteComposerQuotedPostQuery = graphql`
+const noteComposerQuotedPostQuery = graphql`
   query NoteComposerQuotedPostQuery($id: ID!) {

Then update the usage at line 116:

       environment(),
-      NoteComposerQuotedPostQuery,
+      noteComposerQuotedPostQuery,
       { id },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-next/src/components/NoteComposer.tsx` at line 44, Rename the const query
to a different identifier to avoid shadowing the imported type
NoteComposerQuotedPostQuery (e.g. NoteComposerQuotedPostQueryDoc or
NoteComposerQuotedPost_Query) and update all usages where the query const is
passed (the place that currently references NoteComposerQuotedPostQuery as the
query variable inside the NoteComposer component) to the new const name; ensure
imports of the type NoteComposerQuotedPostQuery remain unchanged and that only
the const query identifier is renamed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web-next/src/components/NoteComposer.tsx`:
- Around line 127-130: The code is dereferencing node.actor (actorHandle and
actorAvatarUrl) without null checks; update the assignment in NoteComposer.tsx
to safely access actor fields—either add an early guard that returns/omits
creating the object when node.actor is null, or use optional chaining/fallbacks
(e.g., actorHandle: node.actor?.handle ?? undefined, actorAvatarUrl:
node.actor?.avatarUrl ?? undefined) so actorName, actorHandle, and
actorAvatarUrl cannot throw when node.actor is missing.

---

Nitpick comments:
In `@web-next/src/components/NoteComposer.tsx`:
- Line 44: Rename the const query to a different identifier to avoid shadowing
the imported type NoteComposerQuotedPostQuery (e.g.
NoteComposerQuotedPostQueryDoc or NoteComposerQuotedPost_Query) and update all
usages where the query const is passed (the place that currently references
NoteComposerQuotedPostQuery as the query variable inside the NoteComposer
component) to the new const name; ensure imports of the type
NoteComposerQuotedPostQuery remain unchanged and that only the const query
identifier is renamed.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6714445 and ded2044.

📒 Files selected for processing (5)
  • web-next/src/components/AppSidebar.tsx
  • web-next/src/components/NoteComposeModal.tsx
  • web-next/src/components/NoteComposer.tsx
  • web-next/src/components/PostControls.tsx
  • web-next/src/contexts/NoteComposeContext.tsx

Copy link

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

🧹 Nitpick comments (1)
web-next/src/components/NoteComposer.tsx (1)

1-4: Reorder imports so external modules come before internal modules.

~/lib/langdet.ts is currently above package imports. Please keep package imports first, then internal imports.

💡 Suggested import reorder
-import { detectLanguage } from "~/lib/langdet.ts";
 import { fetchQuery, graphql } from "relay-runtime";
 import { createEffect, createSignal, onCleanup, Show } from "solid-js";
 import { createMutation, useRelayEnvironment } from "solid-relay";
+import { detectLanguage } from "~/lib/langdet.ts";

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Imports: External imports first, internal imports second (alphabetically within groups).

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

In `@web-next/src/components/NoteComposer.tsx` around lines 1 - 4, The import
order in NoteComposer.tsx is incorrect: move external package imports
(fetchQuery, graphql from "relay-runtime"; createEffect, createSignal,
onCleanup, Show from "solid-js"; createMutation, useRelayEnvironment from
"solid-relay") above the internal import (detectLanguage from
"~/lib/langdet.ts"), and alphabetize imports within each group per the project
style; update the top-of-file import block so all third‑party modules appear
first and the local "~/lib/langdet.ts" import comes after.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web-next/src/components/NoteComposer.tsx`:
- Around line 257-264: In NoteComposer's remove-quote button (the button with
onClick={() => props.onQuoteRemoved?.()} that renders <IconX class="size-4" />),
add an accessible name by adding an aria-label (for example aria-label="Remove
quoted text" or similar) to the button element; keep the existing title if
desired but ensure aria-label is present so screen readers receive a reliable
name for the icon-only control.
- Around line 109-122: The NoteComposer currently risks submitting a stale
quotedPostId when the fetch for props.quotedPostId returns null/unsupported;
update the fetchQuery subscription in NoteComposer (where
NoteComposerQuotedPostQuery is used) to call setQuotedPost(null) whenever
data.node is missing or node.__typename is not "Note"/"Article" so the preview
is cleared; also ensure the submit handler (instead of reading
props.quotedPostId at submission) reads the current quotedPost state and omits
the quote from the payload or prevents submission if quotedPost is null so no
invisible/stale quote association can be sent.
- Line 22: The imported types NoteComposerQuotedPostQuery and
NoteComposerMutation conflict with template literal constants currently named
the same; rename the graphql template literal constants to camelCase
(noteComposerQuotedPostQuery and noteComposerMutation) and update all usages
where those constants are referenced (e.g., in the createFragment/commit
mutation calls that currently reference NoteComposerQuotedPostQuery and
NoteComposerMutation) so the imported types remain distinct from the runtime
constants.

---

Nitpick comments:
In `@web-next/src/components/NoteComposer.tsx`:
- Around line 1-4: The import order in NoteComposer.tsx is incorrect: move
external package imports (fetchQuery, graphql from "relay-runtime";
createEffect, createSignal, onCleanup, Show from "solid-js"; createMutation,
useRelayEnvironment from "solid-relay") above the internal import
(detectLanguage from "~/lib/langdet.ts"), and alphabetize imports within each
group per the project style; update the top-of-file import block so all
third‑party modules appear first and the local "~/lib/langdet.ts" import comes
after.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 165a777 and e7c8e27.

📒 Files selected for processing (1)
  • web-next/src/components/NoteComposer.tsx

@malkoG malkoG force-pushed the feature/quoting-on-web-next branch from e7c8e27 to dc3921b Compare March 1, 2026 12:00
Copy link

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web-next/src/components/NoteComposer.tsx (1)

188-196: ⚠️ Potential issue | 🟡 Minor

Consider preventing submission of unverified quote associations.

If the quoted post fetch fails or returns unsupported data, quotedPost() will be null while props.quotedPostId may still be set. This could create a quote association that the user cannot verify in the UI.

🛡️ Suggested defensive fix
       input: {
         content: noteContent,
         language: language()?.baseName ?? i18n.locale,
         visibility: visibility(),
-        quotedPostId: props.quotedPostId ?? null,
+        quotedPostId: quotedPost() ? (props.quotedPostId ?? null) : null,
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-next/src/components/NoteComposer.tsx` around lines 188 - 196, The
createNote submission currently uses props.quotedPostId even when quotedPost()
may be null (fetch failed or unsupported), risking creation of an unverifiable
quote; update the submission logic in NoteComposer so that the
variables.input.quotedPostId is only set when quotedPost() is non-null and valid
(e.g., quotedPost() !== null/undefined and has expected fields); otherwise pass
null. Locate the createNote call in NoteComposer and guard the quotedPostId with
a conditional that prefers quotedPost() presence over props.quotedPostId to
ensure the backend only receives verified quote associations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@web-next/src/components/NoteComposer.tsx`:
- Around line 188-196: The createNote submission currently uses
props.quotedPostId even when quotedPost() may be null (fetch failed or
unsupported), risking creation of an unverifiable quote; update the submission
logic in NoteComposer so that the variables.input.quotedPostId is only set when
quotedPost() is non-null and valid (e.g., quotedPost() !== null/undefined and
has expected fields); otherwise pass null. Locate the createNote call in
NoteComposer and guard the quotedPostId with a conditional that prefers
quotedPost() presence over props.quotedPostId to ensure the backend only
receives verified quote associations.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7c8e27 and dc3921b.

📒 Files selected for processing (10)
  • web-next/src/components/AppSidebar.tsx
  • web-next/src/components/NoteComposeModal.tsx
  • web-next/src/components/NoteComposer.tsx
  • web-next/src/components/PostControls.tsx
  • web-next/src/contexts/NoteComposeContext.tsx
  • web-next/src/locales/en-US/messages.po
  • web-next/src/locales/ja-JP/messages.po
  • web-next/src/locales/ko-KR/messages.po
  • web-next/src/locales/zh-CN/messages.po
  • web-next/src/locales/zh-TW/messages.po
🚧 Files skipped from review as they are similar to previous changes (7)
  • web-next/src/components/PostControls.tsx
  • web-next/src/components/AppSidebar.tsx
  • web-next/src/locales/ja-JP/messages.po
  • web-next/src/locales/en-US/messages.po
  • web-next/src/locales/zh-CN/messages.po
  • web-next/src/locales/ko-KR/messages.po
  • web-next/src/contexts/NoteComposeContext.tsx

@malkoG malkoG force-pushed the feature/quoting-on-web-next branch from dc3921b to 392c37c Compare March 1, 2026 12:23
Copy link

@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 (1)
web-next/src/components/NoteComposer.tsx (1)

44-60: Consider fetching a lightweight preview field instead of full content for quotes.

The composer preview only needs a snippet, but the query pulls full HTML content. For long articles/notes this inflates payload and render cost in a hot UI path.

Also applies to: 134-139, 252-257

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

In `@web-next/src/components/NoteComposer.tsx` around lines 44 - 60, The query
NoteComposerQuotedPostQuery (and the other NoteComposer quote fetches) currently
requests full HTML `content`; change those selections to a lightweight preview
field instead (e.g., `contentPreview`, `excerpt`, `summary`, or use a truncation
arg if your schema supports something like `content(truncate: 200)`) so the
composer only pulls a short snippet and actor info; update the
NoteComposerQuotedPostQuery selection set and the other two quote query
locations to use that preview field and adjust any consuming JSX/props to render
the snippet instead of full `content`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web-next/src/components/NoteComposer.tsx`:
- Around line 44-60: The query NoteComposerQuotedPostQuery (and the other
NoteComposer quote fetches) currently requests full HTML `content`; change those
selections to a lightweight preview field instead (e.g., `contentPreview`,
`excerpt`, `summary`, or use a truncation arg if your schema supports something
like `content(truncate: 200)`) so the composer only pulls a short snippet and
actor info; update the NoteComposerQuotedPostQuery selection set and the other
two quote query locations to use that preview field and adjust any consuming
JSX/props to render the snippet instead of full `content`.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc3921b and 392c37c.

📒 Files selected for processing (10)
  • web-next/src/components/AppSidebar.tsx
  • web-next/src/components/NoteComposeModal.tsx
  • web-next/src/components/NoteComposer.tsx
  • web-next/src/components/PostControls.tsx
  • web-next/src/contexts/NoteComposeContext.tsx
  • web-next/src/locales/en-US/messages.po
  • web-next/src/locales/ja-JP/messages.po
  • web-next/src/locales/ko-KR/messages.po
  • web-next/src/locales/zh-CN/messages.po
  • web-next/src/locales/zh-TW/messages.po
✅ Files skipped from review due to trivial changes (1)
  • web-next/src/locales/ja-JP/messages.po
🚧 Files skipped from review as they are similar to previous changes (2)
  • web-next/src/locales/ko-KR/messages.po
  • web-next/src/locales/en-US/messages.po

malkoG added 2 commits March 1, 2026 21:37
Add quotedPostId support to the NoteCompose context, fetch and display
a quoted post preview in NoteComposer, and connect the quote button in
PostControls to open the composer with the quoted post ID.
Re-extract message catalogs after adding aria-label to the remove quote
button, and restore the "Remove quote" translations for all locales.
@malkoG malkoG force-pushed the feature/quoting-on-web-next branch from 392c37c to 507c934 Compare March 1, 2026 12:37
@dahlia dahlia requested a review from Copilot March 2, 2026 05:57
Copy link
Member

@dahlia dahlia left a comment

Choose a reason for hiding this comment

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

Overall the implementation is clean and the major issues raised in prior review discussions (actor null guard, submit button timing, aria-label) have been well addressed. A few things to follow up on below.

<SidebarMenuItem class="list-none">
<SidebarMenuButton
onClick={openNoteCompose}
onClick={() => openNoteCompose()}
Copy link
Member

Choose a reason for hiding this comment

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

This looks like a style change but it is actually a necessary bug fix. Since open's signature changed to (quotedPostId?: string) => void, the original onClick={openNoteCompose} would have passed the MouseEvent object as quotedPostId, causing incorrect behavior. Worth calling out explicitly in the commit message.

node(id: $id) {
... on Note {
__typename
content
Copy link
Member

Choose a reason for hiding this comment

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

The content field here is rendered via innerHTML. Could you confirm that this GraphQL field returns server-sanitized HTML rather than the Markdown source? It is easy to conflate with the content field on the CreateNotePayload in NoteComposerMutation (same file), which likely returns the Markdown source.

<div class="flex items-start gap-3 rounded-md border border-input bg-muted/50 p-3">
<Avatar class="size-8 flex-shrink-0">
<AvatarImage src={qp().actorAvatarUrl} />
</Avatar>
Copy link
Member

Choose a reason for hiding this comment

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

No <AvatarFallback> is provided. If the avatar URL fails to load (common with federated accounts), the space will be blank. Consider adding a fallback with initials or a default icon.

},
});
onCleanup(() => subscription.unsubscribe());
});
Copy link
Member

Choose a reason for hiding this comment

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

The legacy web/ Composer.tsx supports paste-to-quote: when the user pastes a URL into the text area, it fetches the post at that URL and offers to attach it as a quote (only for public/unlisted posts). See the implementation here.

This is a core part of the quoting UX and a notable feature parity gap. Could this be included in this PR?

The rough flow from web/:

  1. On paste, check if the clipboard text is a valid URL
  2. Fetch /api/posts?iri=... to resolve the post
  3. Only offer to quote if visibility is public or unlisted
  4. Ask for confirmation, then set quotedPostId

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR wires up “quote” composition in the web-next/ Solid + Relay app by passing a quoted post ID through the compose modal flow, showing a quoted-post preview in the composer, and sending the quote association to the createNote mutation.

Changes:

  • Extend NoteComposeContext to track quotedPostId, open the composer with an optional quoted post ID, and allow clearing the quote.
  • Hook the Quote button in PostControls to open the compose modal pre-populated with the quoted post.
  • Add a quoted-post preview UI + “Remove quote” i18n strings, and pass quotedPostId into the note creation mutation.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
web-next/src/contexts/NoteComposeContext.tsx Adds quotedPostId signal + APIs to open/clear quote state.
web-next/src/components/PostControls.tsx Wires Quote button to open composer with the selected post ID.
web-next/src/components/NoteComposeModal.tsx Shows “Quote” title when quoting and passes quote props to composer.
web-next/src/components/NoteComposer.tsx Fetches and renders quoted-post preview; includes remove-quote action; passes quotedPostId into mutation input.
web-next/src/components/AppSidebar.tsx Updates compose button click handler to call open explicitly (avoid accidental event arg).
web-next/src/locales/en-US/messages.po Adds/updates translation entries including “Remove quote” and updated source refs.
web-next/src/locales/ja-JP/messages.po Adds/updates translation entries including “Remove quote” and updated source refs.
web-next/src/locales/ko-KR/messages.po Adds/updates translation entries including “Remove quote” and updated source refs.
web-next/src/locales/zh-CN/messages.po Adds/updates translation entries including “Remove quote” and updated source refs.
web-next/src/locales/zh-TW/messages.po Adds/updates translation entries including “Remove quote” and updated source refs.

Comment on lines 11 to +15
isOpen: () => boolean;
open: () => void;
quotedPostId: () => string | null;
open: (quotedPostId?: string) => void;
close: () => void;
clearQuote: () => void;
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

open now accepts an optional quotedPostId, which makes it easy to accidentally pass an event object when using it as a UI handler (e.g., onSelect={openNoteCompose} / onClick={openNoteCompose}). This can result in quotedPostId being set to a non-ID value at runtime and break quote fetching / modal state. Consider separating the APIs (e.g., keep open() parameterless and add openWithQuote(id: string)), or otherwise ensure all callers wrap it to avoid passing events.

Copilot uses AI. Check for mistakes.
Comment on lines +235 to +237
{/* Quoted post preview */}
<Show when={quotedPost()}>
{(qp) => (
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The quote preview is only rendered when quotedPost() is non-null. If quotedPostId is set but the fetch fails (or the node isn't accessible), the user won't see the preview or the "Remove quote" button, leaving no in-UI way to clear the quote.

Copilot uses AI. Check for mistakes.
<Button type="submit" disabled={isCreating()}>
<Button
type="submit"
disabled={isCreating() || (!!props.quotedPostId && !quotedPost())}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The submit button is disabled whenever quotedPostId is set but quotedPost() is still null. Combined with the preview being hidden in that state, this can permanently block composing if the quoted-post preview fails to load. Suggest tracking a separate loading/error state and always allowing either (a) clearing the quote, or (b) submitting with quotedPostId even if preview fetch fails.

Suggested change
disabled={isCreating() || (!!props.quotedPostId && !quotedPost())}
disabled={isCreating()}

Copilot uses AI. Check for mistakes.
Comment on lines 1 to +4
import { detectLanguage } from "~/lib/langdet.ts";
import { graphql } from "relay-runtime";
import { createEffect, createSignal, Show } from "solid-js";
import { createMutation } from "solid-relay";
import { fetchQuery, graphql } from "relay-runtime";
import { createEffect, createSignal, onCleanup, Show } from "solid-js";
import { createMutation, useRelayEnvironment } from "solid-relay";
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

Import ordering here doesn't follow the repo convention of grouping external imports before internal imports (per AGENTS.md). Please reorder the imports so relay-runtime / solid-js / solid-relay come before ~/... imports.

Copilot uses AI. Check for mistakes.
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.

3 participants