diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 9642c06be90..953195b606c 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -58,6 +58,19 @@ "ghost/admin/lib/koenig-editor/package.json" ], "packageRules": [ + // Always require dashboard approval for major updates + // This was largely to avoid the noise of major updates which were ESM only + // The idea was to check and accept major updates if they were NOT ESM + // But this hasn't been workable with our capacity + // Plus, ESM-only is an edge case in the grand scheme of dependencies + { + "description": "Require dashboard approval for major updates", + "matchUpdateTypes": [ + "major" + ], + "dependencyDashboardApproval": true + }, + // Group NQL packages separately from other TryGhost packages { "groupName": "NQL packages", diff --git a/apps/admin-x-framework/src/api/members.ts b/apps/admin-x-framework/src/api/members.ts index 5332bb6a8a7..5af63c4fd08 100644 --- a/apps/admin-x-framework/src/api/members.ts +++ b/apps/admin-x-framework/src/api/members.ts @@ -6,6 +6,11 @@ export type Member = { email?: string; avatar_image?: string; can_comment?: boolean; + commenting?: { + disabled: boolean; + disabled_reason?: string; + disabled_until?: string; + }; }; export interface MembersResponseType { @@ -22,12 +27,13 @@ export const useBrowseMembers = createQuery({ export const useDisableMemberCommenting = createMutation< MembersResponseType, - {id: string; reason: string} + {id: string; reason: string; hideComments?: boolean} >({ method: 'POST', path: ({id}) => `/members/${id}/commenting/disable`, - body: ({reason}) => ({ - reason + body: ({reason, hideComments}) => ({ + reason, + hide_comments: hideComments }), invalidateQueries: { dataType: 'CommentsResponseType' diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx index daf660977f0..4b484ce5707 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx @@ -67,6 +67,10 @@ const features: Feature[] = [{ title: 'Disable Member Commenting', description: 'Allow staff to disable commenting for individual members', flag: 'disableMemberCommenting' +}, { + title: 'Hide Comments When Disabling', + description: 'Show option to hide all previous comments when disabling commenting for a member', + flag: 'disableMemberCommentingHideComments' }, { title: 'Sniper Links', description: 'Enable mail app links on signup/signin', diff --git a/apps/portal/src/components/pages/magic-link-page.js b/apps/portal/src/components/pages/magic-link-page.js index 619ba23ff27..374c585042f 100644 --- a/apps/portal/src/components/pages/magic-link-page.js +++ b/apps/portal/src/components/pages/magic-link-page.js @@ -5,6 +5,7 @@ import SniperLinkButton from '../common/sniper-link-button'; import AppContext from '../../app-context'; import {ReactComponent as EnvelopeIcon} from '../../images/icons/envelope.svg'; import {t} from '../../utils/i18n'; +import {isInviteOnly} from '../../utils/helpers'; export const MagicLinkStyles = ` .gh-portal-icon-envelope { @@ -97,7 +98,8 @@ export default class MagicLinkPage extends React.Component { return { signin: { withOTC: t('An email has been sent to {submittedEmailOrInbox}. Click the link inside or enter your code below.', {submittedEmailOrInbox}), - withoutOTC: t('A login link has been sent to your inbox. If it doesn\'t arrive in 3 minutes, be sure to check your spam folder.') + withoutOTC: t('A login link has been sent to your inbox. If it doesn\'t arrive in 3 minutes, be sure to check your spam folder.'), + withoutOTCInviteOnly: t('If you have an account, a sign in link will be sent to you shortly. Please check your inbox and spam folder.') }, signup: t('To complete signup, click the confirmation link in your inbox. If it doesn\'t arrive within 3 minutes, check your spam folder!') }; @@ -119,7 +121,16 @@ export default class MagicLinkPage extends React.Component { return descriptionConfig.signup; } - return otcRef ? descriptionConfig.signin.withOTC : descriptionConfig.signin.withoutOTC; + if (otcRef) { + return descriptionConfig.signin.withOTC; + } + + const {site} = this.context; + if (isInviteOnly({site})) { + return descriptionConfig.signin.withoutOTCInviteOnly; + } + + return descriptionConfig.signin.withoutOTC; } renderFormHeader() { diff --git a/apps/portal/src/utils/errors.js b/apps/portal/src/utils/errors.js index 651251de55d..90bd4ce6d02 100644 --- a/apps/portal/src/utils/errors.js +++ b/apps/portal/src/utils/errors.js @@ -50,8 +50,8 @@ export function chooseBestErrorMessage(error, alreadyTranslatedDefaultMessage) { if (specialMessages.length === 0) { // This formatting is intentionally weird. It causes the i18n-parser to pick these strings up. // Do not redefine this t. It's a local function and needs to stay that way. - t('No member exists with this e-mail address. Please sign up first.'); - t('No member exists with this e-mail address.'); + t('No member exists with this email address. Please sign up first.'); + t('No member exists with this email address.'); t('This site is invite-only, contact the owner for access.'); t('Unable to initiate checkout session'); t('This site is not accepting payments at the moment.'); diff --git a/apps/portal/test/data-attributes.test.js b/apps/portal/test/data-attributes.test.js index 68e962c58d9..8cbb0670117 100644 --- a/apps/portal/test/data-attributes.test.js +++ b/apps/portal/test/data-attributes.test.js @@ -934,7 +934,7 @@ describe('Portal Data attributes:', () => { }) .mockResolvedValueOnce({ ok: false, - json: async () => ({errors: [{message: 'No member exists with this e-mail address. Please sign up first.'}]}), + json: async () => ({errors: [{message: 'No member exists with this email address. Please sign up first.'}]}), status: 400 }); @@ -942,7 +942,7 @@ describe('Portal Data attributes:', () => { expect(window.fetch).toHaveBeenCalledTimes(2); expect(form.classList.add).toHaveBeenCalledWith('error'); - expect(errorEl.innerText).toBe('No member exists with this e-mail address. Please sign up first.'); + expect(errorEl.innerText).toBe('No member exists with this email address. Please sign up first.'); }); }); }); diff --git a/apps/posts/src/views/comments/comments.tsx b/apps/posts/src/views/comments/comments.tsx index 5fed30735c0..3abd0777127 100644 --- a/apps/posts/src/views/comments/comments.tsx +++ b/apps/posts/src/views/comments/comments.tsx @@ -15,6 +15,7 @@ const Comments: React.FC = () => { const {data: configData} = useBrowseConfig(); const commentPermalinksEnabled = configData?.config?.labs?.commentPermalinks === true; const disableMemberCommentingEnabled = configData?.config?.labs?.disableMemberCommenting === true; + const hideCommentsEnabled = configData?.config?.labs?.disableMemberCommentingHideComments === true; const handleAddFilter = useCallback((field: string, value: string, operator: string = 'is') => { setFilters((prevFilters) => { @@ -87,6 +88,7 @@ const Comments: React.FC = () => { disableMemberCommentingEnabled={disableMemberCommentingEnabled} fetchNextPage={fetchNextPage} hasNextPage={hasNextPage} + hideCommentsEnabled={hideCommentsEnabled} isFetchingNextPage={isFetchingNextPage} isLoading={isFetching && !isFetchingNextPage} items={data?.comments ?? []} diff --git a/apps/posts/src/views/comments/components/comments-list.tsx b/apps/posts/src/views/comments/components/comments-list.tsx index 74318f619d5..e1c1edbb495 100644 --- a/apps/posts/src/views/comments/components/comments-list.tsx +++ b/apps/posts/src/views/comments/components/comments-list.tsx @@ -9,6 +9,7 @@ import { AlertDialogTitle, Badge, Button, + Checkbox, Dialog, DialogContent, DialogDescription, @@ -19,6 +20,7 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + Label, LucideIcon, Tooltip, TooltipContent, @@ -135,7 +137,8 @@ function CommentsList({ onAddFilter, isLoading, commentPermalinksEnabled, - disableMemberCommentingEnabled + disableMemberCommentingEnabled, + hideCommentsEnabled }: { items: Comment[]; totalItems: number; @@ -146,6 +149,7 @@ function CommentsList({ isLoading?: boolean; commentPermalinksEnabled?: boolean; disableMemberCommentingEnabled?: boolean; + hideCommentsEnabled?: boolean; }) { const parentRef = useRef(null); @@ -168,6 +172,7 @@ function CommentsList({ const {mutate: enableCommenting} = useEnableMemberCommenting(); const [commentToDelete, setCommentToDelete] = useState(null); const [memberToDisable, setMemberToDisable] = useState<{member: Comment['member']; commentId: string} | null>(null); + const [hideComments, setHideComments] = useState(false); const confirmDelete = () => { if (commentToDelete) { @@ -180,9 +185,11 @@ function CommentsList({ if (memberToDisable?.member?.id) { disableCommenting({ id: memberToDisable.member.id, - reason: `Disabled from comment ${memberToDisable.commentId}` + reason: `Disabled from comment ${memberToDisable.commentId}`, + ...(hideCommentsEnabled && {hideComments}) }); setMemberToDisable(null); + setHideComments(false); } }; @@ -450,6 +457,7 @@ function CommentsList({ { if (!open) { setMemberToDisable(null); + setHideComments(false); } }}> @@ -461,8 +469,24 @@ function CommentsList({ + {hideCommentsEnabled && ( +
+ setHideComments(checked === true)} + /> + +
+ )} + - {{else}}