Skip to content

Conversation

@alamenai
Copy link

@alamenai alamenai commented Dec 21, 2025

What

Before the changes

In the comments-list component, we define the TooltipProvider component inside a loop where we recreate new instance per iteration.

Each TooltipProvider creates a new Context.

With virtualized scrolling showing ~20 visible rows, that's:

4 providers × 20 rows = 80 context provider instances.

TooltipProvider manages shared state for tooltip behavior:

  // From Radix UI TooltipProvider props
  interface TooltipProviderProps {
    delayDuration?: number;      // delay before showing (default 700ms)
    skipDelayDuration?: number;  // time window to skip delay when moving between tooltips
    disableHoverableContent?: boolean;
  }

With separate providers, tooltips can't coordinate.

When a user hovers from "Replies" → "Likes" → "Reports", the default delay is 700ms, for 3 TooltipProvider instances, this becomes 2100ms.

The skipDelayDuration feature (which shows subsequent tooltips immediately) only works within the same provider.

From the internal implementation of TooltipProvider , each instance manages its own state, and based on the current implementation each comment row creates 4 state objects ( Because we declare 4 Tooltip Provider inside a loop ), that means for 20 comments, this is 80 state objects are created and 80 extra nodes in the tree.

After the changes

I extracted the TooltipProvider to wrap the whole comment-list component once time ( Though it would be better to lift it up to the app-level provider but I wanted to avoid many files changes).

Now, we have only one instance of the TooltipProvider shared between the comments wether we have only one comment or 1000 comments.

This will create only 1 shared context node in the tree per list of comments.


Note

Wraps the entire comments list in a single TooltipProvider, removing redundant per-item providers to reduce context overhead and unify tooltip behavior.

  • Comments List (apps/posts/src/views/comments/components/comments-list.tsx)
    • Wrap the whole table in a single TooltipProvider.
    • Remove duplicated TooltipProvider instances inside row content (timestamp and stats tooltips now share the parent provider).
    • No UI changes; reduces context/provider instances and enables shared tooltip timing behavior across tooltips.

Written by Cursor Bugbot for commit 602becf. This will update automatically on new commits. Configure here.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 21, 2025

Walkthrough

The comments list component is being restructured to wrap content in a TooltipProvider for enhanced tooltip functionality. Multiple new UI elements are introduced: conditional author avatar and name display with filter actions, post context information with optional filter buttons, post feature images, dropdown menus providing "View post" and "View member" actions, and Show/Hide controls for published comments. Date display is enhanced with tooltips showing formatted dates. The delete confirmation dialog structure is realigned with the updated wrapper. Existing virtualization mechanics and placeholders are preserved. The component's public signature remains unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20-25 minutes

  • Author info section: Verify conditional rendering logic for avatar/name display and click handler for filter actions
  • Post context and feature image display: Ensure proper data binding and null-safety for post details
  • Dropdown menu implementation: Review the new DropdownMenu integration for "View post" and "View member" options
  • Show/Hide comment controls: Validate state management and conditional rendering logic for published vs. hidden states
  • Tooltip enhancements: Check date formatting logic and TooltipProvider wrapping scope
  • Delete confirmation dialog alignment: Confirm proper open state handling and event flow with updated structure
  • Virtualization compatibility: Verify that existing virtualization mechanics function correctly with the restructured layout

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main refactoring objective: moving TooltipProvider from inside loops to wrap the entire component, reducing context overhead and duplication.
Description check ✅ Passed The description thoroughly explains the problem (80 context instances with 20 visible rows), the solution (single shared provider), and the benefits (reduced overhead and unified tooltip behavior).
✨ Finishing touches
  • 📝 Generate docstrings
🧪 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.

Copy link
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

Caution

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

⚠️ Outside diff range comments (1)
apps/posts/src/views/comments/components/comments-list.tsx (1)

163-170: Delete functionality is incomplete – dialog cannot be triggered.

The commentToDelete state and confirmDelete function are defined, and an AlertDialog exists (lines 373-391), but there is no UI element that calls setCommentToDelete(item) to trigger the delete flow. This results in unreachable code.

If delete functionality is planned for a future PR, consider removing this code and adding it when the feature is complete. Otherwise, add a delete button (e.g., in the dropdown menu).

🔎 Example: Add delete option to the dropdown menu
                                                        {item.member?.id && (
                                                            <DropdownMenuItem asChild>
                                                                <a href={`#/members/${item.member.id}`}>
                                                                    <LucideIcon.User className="mr-2 size-4" />
                                                                    View member
                                                                </a>
                                                            </DropdownMenuItem>
                                                        )}
+                                                       <DropdownMenuItem
+                                                           className="text-destructive focus:text-destructive"
+                                                           onClick={() => setCommentToDelete(item)}
+                                                       >
+                                                           <LucideIcon.Trash2 className="mr-2 size-4" />
+                                                           Delete
+                                                       </DropdownMenuItem>
                                                    </DropdownMenuContent>
🧹 Nitpick comments (2)
apps/posts/src/views/comments/components/comments-list.tsx (2)

87-127: Consider debouncing the resize event listener.

The checkIfClamped function is called on every resize event without debouncing. With many comment rows visible (virtualized list with ~20 visible items), this could trigger many layout calculations during window resizing.

🔎 Proposed fix using a debounced resize handler
+import {forwardRef, useEffect, useRef, useState, useMemo} from 'react';
+
+// Add a simple debounce utility or import from lodash
+function debounce<T extends (...args: unknown[]) => void>(fn: T, ms: number): T {
+    let timeoutId: ReturnType<typeof setTimeout>;
+    return ((...args: unknown[]) => {
+        clearTimeout(timeoutId);
+        timeoutId = setTimeout(() => fn(...args), ms);
+    }) as T;
+}

 function CommentContent({item}: {item: Comment}) {
     const contentRef = useRef<HTMLDivElement>(null);
     const [isClamped, setIsClamped] = useState(false);
     const [isExpanded, setIsExpanded] = useState(false);

+    const checkIfClamped = useMemo(() => debounce(() => {
+        if (contentRef.current) {
+            setIsClamped(contentRef.current.scrollHeight > contentRef.current.clientHeight);
+        }
+    }, 100), []);

     useEffect(() => {
-        const checkIfClamped = () => {
-            if (contentRef.current) {
-                setIsClamped(contentRef.current.scrollHeight > contentRef.current.clientHeight);
-            }
-        };
-
         checkIfClamped();
         window.addEventListener('resize', checkIfClamped);
         return () => window.removeEventListener('resize', checkIfClamped);
-    }, [item.html]);
+    }, [item.html, checkIfClamped]);

325-353: Consider hiding dropdown trigger when no actions are available.

If both item.post?.url and item.member?.id are falsy, the dropdown menu will render with no items, which is a poor user experience. Consider conditionally rendering the dropdown trigger.

🔎 Proposed fix
+                                               {(item.post?.url || item.member?.id) && (
                                                <DropdownMenu>
                                                    <DropdownMenuTrigger asChild>
                                                        <Button
                                                            className="relative z-10 ml-1"
                                                            size="sm"
                                                            variant="ghost"
                                                        >
                                                            <LucideIcon.Ellipsis />
                                                        </Button>
                                                    </DropdownMenuTrigger>
                                                    <DropdownMenuContent align="start">
                                                        {item.post?.url && (
                                                            <DropdownMenuItem asChild>
                                                                <a href={item.post.url} rel="noopener noreferrer" target="_blank">
                                                                    <LucideIcon.ExternalLink className="mr-2 size-4" />
                                                                    View post
                                                                </a>
                                                            </DropdownMenuItem>
                                                        )}
                                                        {item.member?.id && (
                                                            <DropdownMenuItem asChild>
                                                                <a href={`#/members/${item.member.id}`}>
                                                                    <LucideIcon.User className="mr-2 size-4" />
                                                                    View member
                                                                </a>
                                                            </DropdownMenuItem>
                                                        )}
                                                    </DropdownMenuContent>
                                                </DropdownMenu>
+                                               )}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 31ecb69 and 602becf.

📒 Files selected for processing (1)
  • apps/posts/src/views/comments/components/comments-list.tsx (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (3)
apps/posts/src/views/comments/components/comments-list.tsx (3)

173-173: Good refactor: TooltipProvider consolidation.

Wrapping the entire list with a single TooltipProvider instead of per-row instances correctly addresses the PR objective. This reduces React context overhead and enables tooltip coordination features like skipDelayDuration across all tooltips within the list.


373-391: AlertDialog structure looks good.

The delete confirmation dialog is properly structured with accessible title, description, and action buttons. The red styling for the destructive action is appropriate.

Note: This dialog is currently unreachable (see earlier comment about missing delete trigger).


109-111: Server-side sanitization mitigates XSS risk from dangerouslySetInnerHTML.

The Comment model in the Ghost backend applies restrictive HTML sanitization when comments are saved, allowing only safe tags (p, br, a, blockquote) and enforcing safe link attributes (target="_blank", rel constraints). The item.html received by this component is already sanitized at the API level, making the dangerouslySetInnerHTML usage safe. Consider adding a code comment documenting this server-side sanitization assumption for future maintainers.

Comment on lines +216 to +218
{item.member.avatar_image && (
<div className='absolute inset-0'><img src={item.member.avatar_image} /></div>
)}
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 21, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add empty alt attribute for decorative avatar image.

The avatar image is missing an alt attribute. Since the member name is displayed alongside it, the image is decorative and should have alt="" to be properly ignored by screen readers.

-                                                                    <div className='absolute inset-0'><img src={item.member.avatar_image} /></div>
+                                                                    <div className='absolute inset-0'><img alt="" src={item.member.avatar_image} /></div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{item.member.avatar_image && (
<div className='absolute inset-0'><img src={item.member.avatar_image} /></div>
)}
{item.member.avatar_image && (
<div className='absolute inset-0'><img alt="" src={item.member.avatar_image} /></div>
)}
🤖 Prompt for AI Agents
In apps/posts/src/views/comments/components/comments-list.tsx around lines 216
to 218, the avatar <img> is missing an alt attribute; since the member name is
shown next to it the image is purely decorative, so add an explicit empty alt
attribute (alt="") to the <img> element to ensure screen readers ignore it; if
your project uses an image component wrapper ensure it receives alt="" or
equivalent aria-hidden/role if required by linting.
Copy link
Author

Choose a reason for hiding this comment

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

Yes, I already noticed the missed alt prop in terms of accessibility, but the PR changes means for TooltipProvider. The core team can create a follow-up ticket for this minor issue.

Copy link
Contributor

Choose a reason for hiding this comment

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

@alamenai, that makes sense — keeping the PR focused on the TooltipProvider refactoring is a good approach.

Would you like me to create a GitHub issue to track the missing alt attribute on the avatar image so the core team can address it in a follow-up?


🧠 Learnings used
Learnt from: troyciesco
Repo: TryGhost/Ghost PR: 25271
File: apps/stats/src/views/Stats/Growth/Growth.tsx:199-204
Timestamp: 2025-11-12T22:26:48.725Z
Learning: In apps/stats/src/views/Stats/Growth/Growth.tsx, the loading skeleton for the top posts/pages table intentionally uses colSpan={1} to only span the first (wider) column, creating a visual effect where the skeleton appears only in the main content column (which contains post/page titles with dates).

Learnt from: troyciesco
Repo: TryGhost/Ghost PR: 25271
File: apps/stats/src/views/Stats/Growth/components/GrowthSources.tsx:189-199
Timestamp: 2025-11-12T22:26:03.622Z
Learning: In apps/stats/src/views/Stats/Growth/components/GrowthSources.tsx, the loading skeleton for the GrowthSources table intentionally uses colSpan={1} to only span the first (wider) column, creating a visual effect where the skeleton appears only in the source column which contains the main content (source names with icons).

Learnt from: jonatansberg
Repo: TryGhost/Ghost PR: 25725
File: apps/shade/src/components/ui/filters.tsx:1227-1233
Timestamp: 2025-12-16T12:24:41.848Z
Learning: In apps/shade/src/components/ui/filters.tsx, when displaying a single selected option with detail in the filter list (around line 1227), the label and detail are intentionally rendered horizontally as sibling spans rather than stacked vertically. A stacked layout would break the filter list UI alignment. This is different from the dropdown/popover contexts where vertical stacking is used.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant