Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CORL-2984]: Add ability to select rejection reason for Reject all comments when DSA enabled #4455

Merged
merged 8 commits into from
Jan 11, 2024
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
9 changes: 9 additions & 0 deletions client/src/core/client/admin/components/BanModal.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,12 @@ $ban-modal-text: var(--palette-text-500);
.customizeMessageArrowsIcon {
padding-left: var(--spacing-1);
}

.rejectionReasonLink {
width: fit-content;
color: var(--palette-primary-600) !important;
}

.rejectExistingReason {
background-color: var(--palette-grey-100);
}
89 changes: 82 additions & 7 deletions client/src/core/client/admin/components/BanModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ import React, {
useState,
} from "react";
import { Form } from "react-final-form";
import { graphql } from "react-relay";

import NotAvailable from "coral-admin/components/NotAvailable";
import { PROTECTED_EMAIL_DOMAINS } from "coral-common/common/lib/constants";
import { extractDomain } from "coral-common/common/lib/email";
import {
isOrgModerator,
isSiteModerator,
} from "coral-common/common/lib/permissions/types";
import { useGetMessage } from "coral-framework/lib/i18n";
import { useMutation } from "coral-framework/lib/relay";
import { GQLUSER_ROLE } from "coral-framework/schema";
import { useLocal, useMutation } from "coral-framework/lib/relay";
import { GQLREJECTION_REASON_CODE, GQLUSER_ROLE } from "coral-framework/schema";
import {
ArrowsDownIcon,
ArrowsUpIcon,
Expand All @@ -32,6 +37,7 @@ import {
} from "coral-ui/components/v2";
import { CallOut } from "coral-ui/components/v3";

import { BanModalLocal } from "coral-admin/__generated__/BanModalLocal.graphql";
import { UserStatusChangeContainer_settings } from "coral-admin/__generated__/UserStatusChangeContainer_settings.graphql";
import { UserStatusChangeContainer_user } from "coral-admin/__generated__/UserStatusChangeContainer_user.graphql";
import { UserStatusChangeContainer_viewer } from "coral-admin/__generated__/UserStatusChangeContainer_viewer.graphql";
Expand All @@ -40,16 +46,14 @@ import BanDomainMutation from "./BanDomainMutation";
import BanUserMutation from "./BanUserMutation";
import ModalHeader from "./ModalHeader";
import ModalHeaderUsername from "./ModalHeaderUsername";
import DetailedExplanation from "./ModerationReason/DetailedExplanation";
import Reasons from "./ModerationReason/Reasons";
import RemoveUserBanMutation from "./RemoveUserBanMutation";
import UpdateUserBanMutation from "./UpdateUserBanMutation";
import ChangeStatusModal from "./UserStatus/ChangeStatusModal";
import { getTextForUpdateType } from "./UserStatus/helpers";
import UserStatusSitesList from "./UserStatus/UserStatusSitesList";

import {
isOrgModerator,
isSiteModerator,
} from "coral-common/common/lib/permissions/types";
import styles from "./BanModal.css";

export enum UpdateType {
Expand Down Expand Up @@ -140,6 +144,12 @@ const BanModal: FunctionComponent<Props> = ({
const updateUserBan = useMutation(UpdateUserBanMutation);
const removeUserBan = useMutation(RemoveUserBanMutation);

const [{ dsaFeaturesEnabled }] = useLocal<BanModalLocal>(graphql`
fragment BanModalLocal on Local {
dsaFeaturesEnabled
}
`);

const getMessage = useGetMessage();
const getDefaultMessage = useMemo((): string => {
return getMessage(
Expand Down Expand Up @@ -203,6 +213,17 @@ const BanModal: FunctionComponent<Props> = ({
({ domain }) => domain === emailDomain
);

const [view, setView] = useState<"REASON" | "EXPLANATION">("REASON");
const [reasonCode, setReasonCode] = useState<GQLREJECTION_REASON_CODE | null>(
null
);
const [detailedExplanation, setDetailedExplanation] = useState<string | null>(
null
);
const [otherCustomReason, setOtherCustomReason] = useState<string | null>(
null
);

const canBanDomain =
(viewer.role === GQLUSER_ROLE.ADMIN ||
(viewer.role === GQLUSER_ROLE.MODERATOR && !isSiteModerator(viewer))) &&
Expand All @@ -220,13 +241,22 @@ const BanModal: FunctionComponent<Props> = ({
}, [viewerIsSingleSiteMod, viewer.moderationScopes]);

const onFormSubmit = useCallback(async () => {
const rejectionReason =
rejectExistingComments && dsaFeaturesEnabled
? {
code: reasonCode!,
detailedExplanation,
customReason: otherCustomReason,
}
: undefined;
switch (updateType) {
case UpdateType.ALL_SITES:
try {
await banUser({
userID, // Should be defined because the modal shouldn't open if author is null
message: customizeMessage ? emailMessage : getDefaultMessage,
rejectExistingComments,
rejectionReason,
siteIDs: viewerIsScoped
? viewer?.moderationScopes?.sites?.map(({ id }) => id)
: [],
Expand All @@ -243,6 +273,7 @@ const BanModal: FunctionComponent<Props> = ({
banSiteIDs,
unbanSiteIDs,
rejectExistingComments,
rejectionReason,
});
} catch (err) {
return { [FORM_ERROR]: err.message };
Expand Down Expand Up @@ -281,6 +312,10 @@ const BanModal: FunctionComponent<Props> = ({
removeUserBan,
createDomainBan,
emailDomain,
reasonCode,
detailedExplanation,
otherCustomReason,
dsaFeaturesEnabled,
]);

const {
Expand All @@ -296,7 +331,15 @@ const BanModal: FunctionComponent<Props> = ({
const requiresSiteBanUpdates =
updateType === UpdateType.SPECIFIC_SITES ||
(updateType === UpdateType.ALL_SITES && viewerIsSingleSiteMod);
const disableForm = requiresSiteBanUpdates && !pendingSiteBanUpdates;
const requiresRejectionReasonForDSA =
rejectExistingComments &&
!!dsaFeaturesEnabled &&
(!reasonCode ||
(reasonCode === GQLREJECTION_REASON_CODE.OTHER && !otherCustomReason));

const disableForm =
(requiresSiteBanUpdates && !pendingSiteBanUpdates) ||
requiresRejectionReasonForDSA;

return (
<ChangeStatusModal
Expand Down Expand Up @@ -422,6 +465,38 @@ const BanModal: FunctionComponent<Props> = ({
</CheckBox>
</Localized>
)}
{rejectExistingComments && dsaFeaturesEnabled && (
<Flex
marginTop={2}
direction="column"
marginLeft={2}
padding={2}
className={styles.rejectExistingReason}
>
{view === "REASON" ? (
<Reasons
selected={reasonCode}
onCode={(code) => {
setReasonCode(code);
setView("EXPLANATION");
}}
/>
) : (
<DetailedExplanation
onBack={() => {
setView("REASON");
setReasonCode(null);
}}
code={reasonCode!}
explanationValue={detailedExplanation}
onChangeExplanation={setDetailedExplanation}
customReasonValue={otherCustomReason}
onChangeCustomReason={setOtherCustomReason}
linkClassName={styles.rejectionReasonLink}
/>
)}
</Flex>
)}
</Flex>
{/* EMAIL BAN */}
{canBanDomain && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ const DetailedExplanation: FunctionComponent<Props> = ({
</Localized>
</>
) : (
<AddExplanationButton onClick={() => setShowAddExplanation(true)} />
<AddExplanationButton
onClick={() => setShowAddExplanation(true)}
linkClassName={linkClassName}
/>
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
.optionAction {
padding: 0;
margin: var(--spacing-1) 0;
width: fit-content;
}

.rejectButton {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ const ModerationReason: FunctionComponent<Props> = ({
const [view, setView] = useState<"REASON" | "EXPLANATION">("REASON");
const [reasonCode, setReasonCode] = useState<ReasonCode | null>(null);

const [legalGrounds] = useState<string | null>(null);
const [detailedExplanation, setDetailedExplanation] = useState<string | null>(
null
);
Expand All @@ -41,20 +40,10 @@ const ModerationReason: FunctionComponent<Props> = ({
const submitReason = useCallback(() => {
onReason({
code: reasonCode!,
legalGrounds:
reasonCode === GQLREJECTION_REASON_CODE.ILLEGAL_CONTENT
? legalGrounds
: undefined,
detailedExplanation: detailedExplanation || undefined,
customReason: otherCustomReason || undefined,
});
}, [
reasonCode,
legalGrounds,
detailedExplanation,
onReason,
otherCustomReason,
]);
}, [reasonCode, detailedExplanation, onReason, otherCustomReason]);

return (
<Box className={styles.root} data-testid={`moderation-reason-modal-${id}`}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const BanUserMutation = createMutation(
message: input.message,
rejectExistingComments: input.rejectExistingComments,
siteIDs: input.siteIDs,
rejectionReason: input.rejectionReason,
clientMutationId: clientMutationId.toString(),
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@ const UserBanPopoverContainer: FunctionComponent<Props> = ({
onDismiss,
view,
}) => {
const [{ accessToken }] = useLocal<UserBanPopoverContainer_local>(graphql`
fragment UserBanPopoverContainer_local on Local {
accessToken
}
`);
const [{ accessToken, dsaFeaturesEnabled }] =
useLocal<UserBanPopoverContainer_local>(graphql`
fragment UserBanPopoverContainer_local on Local {
accessToken
dsaFeaturesEnabled
}
`);
const { localeBundles, rootURL } = useCoralContext();
const setSpamBanned = useMutation(SetSpamBanned);
const reject = useMutation(RejectCommentMutation);
Expand Down Expand Up @@ -129,6 +131,12 @@ const UserBanPopoverContainer: FunctionComponent<Props> = ({
userID: user.id,
commentID: comment.id,
rejectExistingComments: !siteBan,
rejectionReason:
dsaFeaturesEnabled && !siteBan
? {
code: GQLREJECTION_REASON_CODE.SPAM,
}
: undefined,
message: getMessage(
localeBundles,
"common-banEmailTemplate",
Expand All @@ -144,14 +152,11 @@ const UserBanPopoverContainer: FunctionComponent<Props> = ({
commentRevisionID: comment.revision.id,
storyID: story.id,
noEmit: true,
reason: {
code: GQLREJECTION_REASON_CODE.OTHER,
detailedExplanation: getMessage(
localeBundles,
"common-userBanned",
"User was banned."
),
},
reason: dsaFeaturesEnabled
? {
code: GQLREJECTION_REASON_CODE.SPAM,
}
: undefined,
});
}
} catch (e) {
Expand Down Expand Up @@ -181,6 +186,7 @@ const UserBanPopoverContainer: FunctionComponent<Props> = ({
setBanError,
siteBan,
setSpamBanned,
dsaFeaturesEnabled,
]);

if (view === "CONFIRM_BAN") {
Expand Down
4 changes: 1 addition & 3 deletions locales/en-US/common.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,4 @@ common-moderationReason-detailedExplanation-placeholder =
common-moderationReason-customReason = Custom reason (required)
common-moderationReason-customReason-placeholder =
.placeholder = Add your reason

common-userBanned =
User was banned.

6 changes: 4 additions & 2 deletions server/src/core/server/graph/mutators/Users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export const Users = (ctx: GraphContext) => ({
message,
rejectExistingComments = false,
siteIDs,
rejectionReason,
}: GQLBanUserInput) =>
ban(
ctx.mongo,
Expand All @@ -346,9 +347,9 @@ export const Users = (ctx: GraphContext) => ({
ctx.user!,
userID,
message,
ctx.i18n,
rejectExistingComments,
siteIDs,
rejectionReason,
ctx.now
),
updateUserBan:
Expand All @@ -358,6 +359,7 @@ export const Users = (ctx: GraphContext) => ({
rejectExistingComments = false,
banSiteIDs,
unbanSiteIDs,
rejectionReason,
}: GQLUpdateUserBanInput) =>
async () =>
updateUserBan(
Expand All @@ -366,13 +368,13 @@ export const Users = (ctx: GraphContext) => ({
ctx.mailerQueue,
ctx.rejectorQueue,
ctx.tenant,
ctx.i18n,
ctx.user!,
userID,
message,
rejectExistingComments,
banSiteIDs,
unbanSiteIDs,
rejectionReason,
ctx.now
),
warn: async (input: GQLWarnUserInput) =>
Expand Down
12 changes: 12 additions & 0 deletions server/src/core/server/graph/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -8875,6 +8875,12 @@ input BanUserInput {
whether or not to reject all the user's previous comments when banning them.
"""
rejectExistingComments: Boolean

"""
rejectionReason is the reason provided for why any existing comments are being
rejected if DSA is enabled
"""
rejectionReason: RejectCommentReasonInput
}

type BanUserPayload {
Expand Down Expand Up @@ -8917,6 +8923,12 @@ input UpdateUserBanInput {
"""
rejectExistingComments: Boolean

"""
rejectionReason is the reason provided for why any existing comments are being
rejected if DSA is enabled
"""
rejectionReason: RejectCommentReasonInput

"""
clientMutationID is required for relay support
"""
Expand Down
Loading
Loading