Skip to content
Merged
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 apps/admin-x-framework/src/api/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type Comment = {
name: string;
email: string;
avatar_image?: string;
can_comment?: boolean;
};
post?: {
id: string;
Expand Down
30 changes: 29 additions & 1 deletion apps/admin-x-framework/src/api/members.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {Meta, createQuery} from '../utils/api/hooks';
import {Meta, createMutation, createQuery} from '../utils/api/hooks';

export type Member = {
id: string;
name?: string;
email?: string;
avatar_image?: string;
can_comment?: boolean;
};

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

export const useDisableMemberCommenting = createMutation<
MembersResponseType,
{id: string; reason: string}
>({
method: 'POST',
path: ({id}) => `/members/${id}/commenting/disable`,
body: ({reason}) => ({
reason
}),
invalidateQueries: {
dataType: 'CommentsResponseType'
}
});

export const useEnableMemberCommenting = createMutation<
MembersResponseType,
{id: string}
>({
method: 'POST',
path: ({id}) => `/members/${id}/commenting/enable`,
body: () => ({}),
invalidateQueries: {
dataType: 'CommentsResponseType'
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,21 @@ const MemberEmailsEditor: React.FC<MemberEmailsEditorProps> = ({
className,
onChange
}) => {
const placeholderSelector = '[&_.koenig-lexical-editor-input-placeholder]';
const contentSelector = '[&_:is(p,blockquote,aside,ul,ol)]';

const baseEditorStyles = cn(
// Base typography
'text-[1.6rem] leading-[1.6] tracking-[-0.01em]',
// Dark mode
'dark:text-white dark:selection:bg-[rgba(88,101,116,0.99)]',
// Placeholder styling
`${placeholderSelector}:font-inter ${placeholderSelector}:text-xl ${placeholderSelector}:tracking-tight`,
'[&_.koenig-lexical-editor-input-placeholder]:font-inter [&_.koenig-lexical-editor-input-placeholder]:text-xl [&_.koenig-lexical-editor-input-placeholder]:tracking-tight',
// Headings dark mode
'[&_:is(h2,h3)]:dark:text-white',

// Content typography
`${contentSelector}:font-inter ${contentSelector}:text-xl ${contentSelector}:tracking-tight`,
'[&_:is(p,blockquote,aside,ul,ol)]:font-inter [&_:is(p,blockquote,aside,ul,ol)]:text-xl [&_:is(p,blockquote,aside,ul,ol)]:tracking-tight',
'[&_:is(h1)]:text-[36px] [&_:is(h2)]:text-[32px] [&_:is(h3)]:text-[26px] [&_:is(h4)]:text-[21px] [&_:is(h5)]:text-[19px] [&_:is(h6)]:text-[19px] [&_:is(h1,h2,h3,h4,h5,h6)]:mb-[0.5em]',
// Horizontal ruler
'[&_:is(hr)]:pt-0',
// Paragraph spacing & bold
'[&_p]:mb-4 [&_strong]:font-semibold'
);
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@tryghost/activitypub": "*",
"@tryghost/admin-x-framework": "*",
"@tryghost/admin-x-settings": "*",
"@tryghost/koenig-lexical": "1.7.8",
"@tryghost/koenig-lexical": "1.7.9",
"@tryghost/posts": "*",
"@tryghost/shade": "*",
"@tryghost/stats": "*",
Expand Down
1 change: 0 additions & 1 deletion apps/comments-ui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,6 @@ const App: React.FC<AppProps> = ({scriptTag, initialCommentId, pageUrl}) => {
commentIdToScrollTo: scrollTargetFound ? initialCommentId : null
});
} catch (e) {
/* eslint-disable no-console */
console.error(`[Comments] Failed to initialize:`, e);
/* eslint-enable no-console */
setState({
Expand Down
1 change: 0 additions & 1 deletion apps/comments-ui/test/unit/utils/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable no-undef */
import setupGhostApi from '../../../src/utils/api';
import {vi} from 'vitest';

Expand Down
1 change: 0 additions & 1 deletion apps/comments-ui/test/unit/utils/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import moment, {DurationInputObject} from 'moment';
import sinon from 'sinon';
import {buildAnonymousMember, buildComment, buildDeletedMember} from '../../utils/fixtures';

/* eslint-disable @typescript-eslint/no-explicit-any */

describe('COMMENT_HASH_PREFIX', function () {
it('exports the correct prefix', function () {
Expand Down
1 change: 0 additions & 1 deletion apps/comments-ui/test/utils/mocked-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const htmlToPlaintext = (html) => {
return html.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
};

/* eslint-disable @typescript-eslint/no-explicit-any */

export class MockedApi {
comments: any[];
Expand Down
1 change: 0 additions & 1 deletion apps/portal/src/components/common/newsletter-management.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ export default function NewsletterManagement({
className="gh-portal-btn-text gh-email-faq-page-button"
onClick={() => doAction('switchPage', {page: 'emailReceivingFAQ', pageData: {direct: false}})}
>
{/* eslint-disable-next-line i18next/no-literal-string */}
{t('Get help')} <span className="right-arrow">&rarr;</span>
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export default class SiteTitleBackButton extends React.Component {
this.context.doAction('closePopup');
}
}}>
{/* eslint-disable-next-line i18next/no-literal-string */}
<span>&larr; </span> {t('Back')}
</button>
</>
Expand Down
1 change: 0 additions & 1 deletion apps/portal/src/components/pages/account-plan-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,6 @@ const RetentionOfferSection = ({offer, onAcceptOffer, onDeclineOffer}) => {
const durationText = formatOfferDuration(offer);

// TODO: Add i18n once copy is finalized
/* eslint-disable i18next/no-literal-string */
return (
<div className="gh-portal-logged-out-form-container gh-portal-retention-offer">
<p className="gh-portal-retention-offer-message">
Expand Down
1 change: 0 additions & 1 deletion apps/portal/src/utils/fixtures.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable no-unused-vars*/
import {getFreeProduct, getMemberData, getOfferData, getPriceData, getProductData, getSiteData, getSubscriptionData, getTestSite} from './fixtures-generator';

export const testSite = getTestSite();
Expand Down
1 change: 0 additions & 1 deletion apps/portal/src/utils/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,6 @@ export function getUrlHistory() {
// Failed to access sessionStorage or something related to that.
// Log a warning, as this shouldn't happen on a modern browser.

/* eslint-disable no-console */
console.warn(`[Portal] Failed to load member URL history:`, error);
}
}
Expand Down
1 change: 0 additions & 1 deletion apps/portal/test/utils/test-fixtures.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable no-unused-vars*/
import {getFreeProduct, getMemberData, getOfferData, getPriceData, getProductData, getSiteData, getSubscriptionData, getNewsletterData} from '../../src/utils/fixtures-generator';

export const transformTierFixture = [
Expand Down
2 changes: 2 additions & 0 deletions apps/posts/src/views/comments/comments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const Comments: React.FC = () => {
const {filters, nql, setFilters, clearFilters, isSingleIdFilter} = useFilterState();
const {data: configData} = useBrowseConfig();
const commentPermalinksEnabled = configData?.config?.labs?.commentPermalinks === true;
const disableMemberCommentingEnabled = configData?.config?.labs?.disableMemberCommenting === true;

const handleAddFilter = useCallback((field: string, value: string, operator: string = 'is') => {
setFilters((prevFilters) => {
Expand Down Expand Up @@ -83,6 +84,7 @@ const Comments: React.FC = () => {
<>
<CommentsList
commentPermalinksEnabled={commentPermalinksEnabled}
disableMemberCommentingEnabled={disableMemberCommentingEnabled}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface CommentsFiltersProps {
filters: Filter[];
onFiltersChange: (filters: Filter[]) => void;
knownPosts: Array<{ id: string; title: string }>;
knownMembers: Array<{ id: string; name: string; email: string }>;
knownMembers: Array<{ id: string; name?: string; email?: string }>;
}

const CommentsFilters: React.FC<CommentsFiltersProps> = ({
Expand Down
107 changes: 95 additions & 12 deletions apps/posts/src/views/comments/components/comments-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import {
AlertDialogTitle,
Badge,
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
Expand All @@ -24,6 +30,7 @@ import {
} from '@tryghost/shade';
import {Comment, useDeleteComment, useHideComment, useShowComment} from '@tryghost/admin-x-framework/api/comments';
import {forwardRef, useEffect, useRef, useState} from 'react';
import {useDisableMemberCommenting, useEnableMemberCommenting} from '@tryghost/admin-x-framework/api/members';
import {useInfiniteVirtualScroll} from '@components/virtual-table/use-infinite-virtual-scroll';
import {useScrollRestoration} from '@components/virtual-table/use-scroll-restoration';

Expand Down Expand Up @@ -127,7 +134,8 @@ function CommentsList({
fetchNextPage,
onAddFilter,
isLoading,
commentPermalinksEnabled
commentPermalinksEnabled,
disableMemberCommentingEnabled
}: {
items: Comment[];
totalItems: number;
Expand All @@ -137,6 +145,7 @@ function CommentsList({
onAddFilter: (field: string, value: string, operator?: string) => void;
isLoading?: boolean;
commentPermalinksEnabled?: boolean;
disableMemberCommentingEnabled?: boolean;
}) {
const parentRef = useRef<HTMLDivElement>(null);

Expand All @@ -155,7 +164,10 @@ function CommentsList({
const {mutate: hideComment} = useHideComment();
const {mutate: showComment} = useShowComment();
const {mutate: deleteComment} = useDeleteComment();
const {mutate: disableCommenting} = useDisableMemberCommenting();
const {mutate: enableCommenting} = useEnableMemberCommenting();
const [commentToDelete, setCommentToDelete] = useState<Comment | null>(null);
const [memberToDisable, setMemberToDisable] = useState<{member: Comment['member']; commentId: string} | null>(null);

const confirmDelete = () => {
if (commentToDelete) {
Expand All @@ -164,6 +176,22 @@ function CommentsList({
}
};

const confirmDisableCommenting = () => {
if (memberToDisable?.member?.id) {
disableCommenting({
id: memberToDisable.member.id,
reason: `Disabled from comment ${memberToDisable.commentId}`
});
setMemberToDisable(null);
}
};

const handleEnableCommenting = (member: Comment['member']) => {
if (member?.id) {
enableCommenting({id: member.id});
}
};

return (
<div ref={parentRef} className="overflow-hidden">
<div
Expand Down Expand Up @@ -202,23 +230,35 @@ function CommentsList({
<div className={`mb-1 flex min-w-0 items-center gap-x-1 text-sm ${item.status === 'hidden' && 'opacity-50'}`}>
<div className='whitespace-nowrap'>
{item.member?.id ? (
<>
<Button
className={`flex h-auto items-center gap-1.5 truncate p-0 font-semibold text-primary hover:opacity-70`}
variant='link'
onClick={() => {
onAddFilter('author', item.member!.id);
}}
>
{item.member.name || 'Unknown'}
</Button>
</>
<Button
className={`flex h-auto items-center gap-1.5 truncate p-0 font-semibold text-primary hover:opacity-70`}
variant='link'
onClick={() => {
onAddFilter('author', item.member!.id);
}}
>
{item.member.name || 'Unknown'}
</Button>
) : (
<span className="block truncate font-semibold">
{item.member?.name || 'Unknown'}
</span>
)}
</div>
{disableMemberCommentingEnabled && item.member?.can_comment === false && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span data-testid="commenting-disabled-indicator">
<LucideIcon.MessageCircleOff
className="size-3.5 text-muted-foreground"
/>
</span>
</TooltipTrigger>
<TooltipContent>Comments disabled</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<LucideIcon.Dot className='shrink-0 text-muted-foreground/50' size={16} />
<div className='shrink-0 whitespace-nowrap'>
{item.created_at && (
Expand Down Expand Up @@ -350,6 +390,24 @@ function CommentsList({
</a>
</DropdownMenuItem>
)}
{disableMemberCommentingEnabled && item.member?.id && (
item.member.can_comment !== false ? (
<DropdownMenuItem onClick={() => {
// Workaround for Radix UI bug where opening Dialog from DropdownMenu
// leaves pointer-events: none on body, freezing the UI
// https://github.com/radix-ui/primitives/issues/3317
queueMicrotask(() => setMemberToDisable({member: item.member, commentId: item.id}));
}}>
<LucideIcon.MessageCircleOff className="mr-2 size-4" />
Disable commenting
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={() => handleEnableCommenting(item.member)}>
<LucideIcon.MessageCircle className="mr-2 size-4" />
Enable commenting
</DropdownMenuItem>
)
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
Expand Down Expand Up @@ -391,6 +449,31 @@ function CommentsList({
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

<Dialog open={!!memberToDisable} onOpenChange={(open) => {
if (!open) {
setMemberToDisable(null);
}
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>Disable comments</DialogTitle>
<DialogDescription>
{memberToDisable?.member?.name || 'This member'} won&apos;t be able to comment
in the future. You can re-enable commenting anytime.
</DialogDescription>
</DialogHeader>

<DialogFooter>
<Button variant="outline" onClick={() => setMemberToDisable(null)}>
Cancel
</Button>
<Button onClick={confirmDisableCommenting}>
Disable comments
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function useKnownFilterValues({comments}: { comments: Comment[] }) {
const posts = new Map<string, { id: string; title: string }>();
const members = new Map<
string,
{ id: string; name: string; email: string }
{ id: string; name?: string; email?: string }
>();
for (const comment of comments) {
if (comment.post?.id && comment.post?.title) {
Expand Down
1 change: 0 additions & 1 deletion apps/posts/test/unit/hooks/use-feature-flag.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {beforeEach, describe, expect, it} from 'vitest';
import {createTestWrapper, mockServer} from '../../utils/msw-helpers';
import {renderHook} from '@testing-library/react';
Expand Down
1 change: 0 additions & 1 deletion apps/posts/test/unit/hooks/use-post-feedback.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {beforeEach, describe, expect, it} from 'vitest';
import {createTestWrapper, endpoint, mockServer, when} from '../../utils/msw-helpers';
import {renderHook, waitFor} from '@testing-library/react';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {beforeEach, describe, expect, it} from 'vitest';
import {createTestWrapper, mockData, mockServer} from '../../utils/msw-helpers';
import {renderHook, waitFor} from '@testing-library/react';
Expand Down
1 change: 0 additions & 1 deletion apps/posts/test/unit/hooks/use-post-referrers.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {beforeEach, describe, expect, it} from 'vitest';
import {createTestWrapper, mockData, mockServer} from '../../utils/msw-helpers';
import {renderHook, waitFor} from '@testing-library/react';
Expand Down
1 change: 0 additions & 1 deletion apps/posts/test/utils/test-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {
Expand Down
Loading
Loading