Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,4 @@ yalc.lock
.env.tinybird
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
.worktrees/
132 changes: 132 additions & 0 deletions apps/admin-x-framework/src/api/comments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {Meta, createInfiniteQuery, createMutation} from '../utils/api/hooks';
import {InfiniteData} from '@tanstack/react-query';

export type CommentMember = {
id: string;
name: string | null;
email: string;
avatar_image: string | null;
commenting_enabled?: boolean;
};

export type CommentPost = {
id: string;
title: string;
slug: string;
url: string;
};

export type Comment = {
id: string;
html: string;
status: 'published' | 'hidden' | 'deleted';
created_at: string;
member_id: string | null;
member?: CommentMember | null;
post_id: string;
post?: CommentPost;
parent_id: string | null;
hidden_at_ban?: boolean;
count?: {
replies?: number;
likes?: number;
};
};

export interface CommentsResponseType {
meta?: Meta;
comments: Comment[];
}

export interface BulkEditResponse {
bulk: {
action: string;
meta: {
stats: {
successful: number;
unsuccessful: number;
};
errors: unknown[];
};
};
}

const dataType = 'CommentsResponseType';

export const useBrowseComments = createInfiniteQuery<CommentsResponseType>({
dataType,
path: '/comments/',
defaultSearchParams: {
include: 'member,post',
limit: '30',
order: 'created_at desc'
},
defaultNextPageParams: (lastPage, params) => {
if (!lastPage.meta?.pagination?.next) {
return undefined;
}
return {
...params,
page: lastPage.meta.pagination.next.toString()
};
},
returnData: (originalData) => {
const {pages} = originalData as InfiniteData<CommentsResponseType>;
const comments = pages.flatMap(page => page.comments);
const meta = pages[pages.length - 1].meta;
return {
comments,
meta,
isEnd: meta ? !meta.pagination.next : true
};
}
});

export const useHideComment = createMutation<CommentsResponseType, string>({
method: 'PUT',
path: id => `/comments/${id}/`,
body: id => ({
comments: [{id, status: 'hidden'}]
}),
invalidateQueries: {dataType}
});

export const useShowComment = createMutation<CommentsResponseType, string>({
method: 'PUT',
path: id => `/comments/${id}/`,
body: id => ({
comments: [{id, status: 'published'}]
}),
invalidateQueries: {dataType}
});

export const useDeleteComment = createMutation<CommentsResponseType, string>({
method: 'PUT',
path: id => `/comments/${id}/`,
body: id => ({
comments: [{id, status: 'deleted'}]
}),
invalidateQueries: {dataType}
});

export interface BulkEditPayload {
filter?: string;
action: 'hide' | 'show' | 'delete';
}

export const useBulkEditComments = createMutation<BulkEditResponse, BulkEditPayload>({
method: 'PUT',
path: () => '/comments/bulk/',
searchParams: payload => {
if (!payload.filter) {
return undefined;
}
return {filter: payload.filter};
},
body: payload => ({
bulk: {
action: payload.action
}
}),
invalidateQueries: {dataType}
});
21 changes: 20 additions & 1 deletion apps/admin-x-framework/src/api/members.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import {Meta, createQuery} from '../utils/api/hooks';
import {Meta, createQuery, createMutation} from '../utils/api/hooks';

export type Member = {
id: string;
name?: string | null;
email?: string;
avatar_image?: string | null;
commenting_enabled?: boolean;
};

export interface MembersResponseType {
Expand All @@ -15,3 +19,18 @@ export const useBrowseMembers = createQuery<MembersResponseType>({
dataType,
path: '/members/'
});

export const useBanMemberFromComments = createMutation<MembersResponseType, string>({
method: 'POST',
path: memberId => `/members/${memberId}/ban-from-comments/`,
invalidateQueries: {dataType}
});

export const useUnbanMemberFromComments = createMutation<MembersResponseType, {memberId: string; restoreComments?: boolean}>({
method: 'POST',
path: ({memberId}) => `/members/${memberId}/unban-from-comments/`,
body: ({restoreComments = true}) => ({
restore_comments: restoreComments
}),
invalidateQueries: {dataType}
});
2 changes: 1 addition & 1 deletion apps/admin-x-framework/src/utils/api/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ interface MutationOptions<ResponseData, Payload> extends Omit<QueryOptions<Respo
path: (payload: Payload) => string;
headers?: Record<string, string>;
body?: (payload: Payload) => FormData | object;
searchParams?: (payload: Payload) => { [key: string]: string; };
searchParams?: (payload: Payload) => { [key: string]: string; } | undefined;
invalidateQueries?: { dataType: string; } | {
filters?: InvalidateQueryFilters<unknown>,
options?: InvalidateOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ const features: Feature[] = [{
title: 'Updated theme translation (beta)',
description: 'Enable theme translation using i18next instead of the old translation package.',
flag: 'themeTranslation'
}, {
title: 'Comment moderation',
description: 'Enable the Comments moderation page and member commenting controls',
flag: 'commentModeration'
}];

const AlphaFeatures: React.FC = () => {
Expand Down
25 changes: 24 additions & 1 deletion apps/admin/src/layout/app-sidebar/nav-content.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react"
import React, { useMemo } from "react"

import {
Button,
Expand All @@ -10,6 +10,7 @@ import {
} from "@tryghost/shade"
import { useCurrentUser } from "@tryghost/admin-x-framework/api/current-user";
import { canManageMembers, canManageTags } from "@tryghost/admin-x-framework/api/users";
import { useBrowseSettings, getSettingValue } from "@tryghost/admin-x-framework/api/settings";
import { NavMenuItem } from "./nav-menu-item";
import NavSubMenu from "./nav-sub-menu";
import { useMemberCount } from "./hooks/use-member-count";
Expand All @@ -19,13 +20,23 @@ import { useEmberRouting } from "@/ember-bridge";

function NavContent({ ...props }: React.ComponentProps<typeof SidebarGroup>) {
const { data: currentUser } = useCurrentUser();
const { data: settingsData } = useBrowseSettings();
const [postsExpanded, setPostsExpanded] = useNavigationExpanded('posts');
const memberCount = useMemberCount();
const routing = useEmberRouting();

const showTags = currentUser && canManageTags(currentUser);
const showMembers = currentUser && canManageMembers(currentUser);

const showComments = useMemo(() => {
if (!showMembers) {
return false;
}
const labsJSON = getSettingValue<string>(settingsData?.settings, 'labs') || '{}';
const labs = JSON.parse(labsJSON);
return labs.commentModeration === true;
}, [showMembers, settingsData?.settings]);

return (
<SidebarGroup {...props}>
<SidebarGroupContent>
Expand Down Expand Up @@ -136,6 +147,18 @@ function NavContent({ ...props }: React.ComponentProps<typeof SidebarGroup>) {
)}
</NavMenuItem>
)}

{showComments && (
<NavMenuItem>
<NavMenuItem.Link
to="comments"
isActive={routing.isRouteActive('comments')}
>
<LucideIcon.MessageSquare />
<NavMenuItem.Label>Comments</NavMenuItem.Label>
</NavMenuItem.Link>
</NavMenuItem>
)}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
Expand Down
8 changes: 3 additions & 5 deletions apps/comments-ui/src/components/content/comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,9 @@ const UnpublishedComment: React.FC<UnpublishedCommentProps> = ({comment, openEdi
: <BlankAvatar />;
const hasReplies = comment.replies && comment.replies.length > 0;

const notPublishedMessage = comment.status === 'hidden' ?
t('This comment has been hidden.') :
comment.status === 'deleted' ?
t('This comment has been removed.') :
'';
const notPublishedMessage = (comment.status === 'hidden' || comment.status === 'deleted') ?
t('[Comment removed by moderator]') :
'';

// currently a reply-to-reply form is displayed inside the top-level PublishedComment component
// so we need to check for a match of either the comment id or the parent id
Expand Down
2 changes: 1 addition & 1 deletion apps/comments-ui/test/e2e/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ test.describe('Actions', async () => {
await deleteComment(page, frame, commentToDelete);

await expect(frame.getByTestId('comment-component')).toHaveCount(2);
await expect(frame.getByText('This comment has been removed')).toBeVisible();
await expect(frame.getByText('[Comment removed by moderator]')).toBeVisible();
await expect(frame.getByTestId('replies-line')).toBeVisible();
});

Expand Down
8 changes: 4 additions & 4 deletions apps/comments-ui/test/e2e/content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ test.describe('Deleted and Hidden Content', async () => {
});

await expect (frame.getByText('This is comment 2')).not.toBeVisible();
await expect (frame.getByText('This comment has been hidden')).toBeVisible();
await expect (frame.getByText('[Comment removed by moderator]').first()).toBeVisible();

await expect (frame.getByText('This is comment 4')).not.toBeVisible();
await expect (frame.getByText('This comment has been removed')).toBeVisible();
await expect (frame.getByText('[Comment removed by moderator]').nth(1)).toBeVisible();
});

test('hides replies that are hidden or deleted', async ({page}) => {
Expand Down Expand Up @@ -132,8 +132,8 @@ test.describe('Deleted and Hidden Content', async () => {

await expect (frame.getByText('This is reply 1')).toBeVisible();
await expect (frame.getByText('This is reply 2')).not.toBeVisible();
// parent comment is hidden but shows text
await expect (frame.getByText('This comment has been hidden')).toBeVisible();
// parent comment is hidden but shows tombstone text
await expect (frame.getByText('[Comment removed by moderator]')).toBeVisible();
});
});

5 changes: 5 additions & 0 deletions apps/posts/src/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Comments from '@views/Comments/comments';
import Growth from '@views/PostAnalytics/Growth/growth';
import Newsletter from '@views/PostAnalytics/Newsletter/newsletter';
import Overview from '@views/PostAnalytics/Overview/overview';
Expand Down Expand Up @@ -62,6 +63,10 @@ export const routes: RouteObject[] = [
}
]
},
{
path: 'comments',
element: <Comments />
},

// Error handling
{
Expand Down
Loading
Loading