Skip to content

Commit

Permalink
Merge pull request #4455 from coralproject/feat/CORL-2984-reject-all-…
Browse files Browse the repository at this point in the history
…comments-dsa

[CORL-2984]: Add ability to select rejection reason for Reject all comments when DSA enabled
  • Loading branch information
nick-funk authored Jan 11, 2024
2 parents ca36016 + 7b253e2 commit 7a2bccb
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 75 deletions.
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 @@ -8970,6 +8970,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 @@ -9012,6 +9018,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

0 comments on commit 7a2bccb

Please sign in to comment.