Skip to content

Commit

Permalink
Merge pull request #4372 from coralproject/reject-reason-backend
Browse files Browse the repository at this point in the history
Reject reasons
  • Loading branch information
marcushaddon authored Nov 13, 2023
2 parents cdd59ca + f13ea54 commit b18e52c
Show file tree
Hide file tree
Showing 33 changed files with 945 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ $moderateCardLinkTextColor: $colors-teal-700;
padding-bottom: var(--spacing-1);
}

.moderationReasonDropdown {
opacity: 1;
}

.moderationReasonCard {
padding: 0;
margin: 0;
}

.storyLabel {
color: var(--palette-grey-500);
font-size: var(--font-size-1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,20 @@ import {
import {
Button,
Card,
ClickOutside,
Dropdown,
Flex,
HorizontalGutter,
Popover,
TextLink,
Timestamp,
} from "coral-ui/components/v2";
import { StarRating } from "coral-ui/components/v3";

import { RejectCommentReasonInput } from "coral-stream/__generated__/RejectCommentMutation.graphql";

import { CommentContent, InReplyTo, UsernameButton } from "../Comment";
import ModerationReason from "../ModerationReason/ModerationReason";
import ApproveButton from "./ApproveButton";
import CommentAuthorContainer from "./CommentAuthorContainer";
import FeatureButton from "./FeatureButton";
Expand Down Expand Up @@ -95,6 +101,8 @@ interface Props {
suspectWords?: Readonly<Readonly<GQLWordlistMatch>[]>;
isArchived?: boolean;
isArchiving?: boolean;
dsaFeaturesEnabled: boolean;
onReason: (reason: RejectCommentReasonInput) => void;
}

const ModerateCard: FunctionComponent<Props> = ({
Expand Down Expand Up @@ -137,6 +145,8 @@ const ModerateCard: FunctionComponent<Props> = ({
suspectWords,
isArchived,
isArchiving,
onReason,
dsaFeaturesEnabled,
}) => {
const div = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -341,22 +351,51 @@ const ModerateCard: FunctionComponent<Props> = ({
</Localized>
)}
<Flex itemGutter>
<RejectButton
onClick={onReject}
invert={status === "rejected"}
disabled={
status === "rejected" ||
dangling ||
deleted ||
readOnly ||
isArchived ||
isArchiving
}
readOnly={readOnly}
className={cn({
[styles.miniButton]: mini,
})}
/>
<Popover
id={`reject-reason-${id}`}
modifiers={{
arrow: { enabled: false },
offset: { offset: "0, 4" },
}}
body={({ toggleVisibility, visible }) => {
return (
<ClickOutside onClickOutside={toggleVisibility}>
<Dropdown className={styles.moderationReasonDropdown}>
<ModerationReason
onReason={onReason}
onCancel={toggleVisibility}
id={id}
/>
</Dropdown>
</ClickOutside>
);
}}
placement="bottom-start"
>
{({ toggleVisibility, visible, ref }) => {
return (
<RejectButton
ref={ref}
toggle={dsaFeaturesEnabled}
open={visible}
onClick={dsaFeaturesEnabled ? toggleVisibility : onReject}
invert={status === "rejected"}
disabled={
status === "rejected" ||
dangling ||
deleted ||
readOnly ||
isArchived ||
isArchiving
}
readOnly={readOnly}
className={cn({
[styles.miniButton]: mini,
})}
/>
);
}}
</Popover>
<ApproveButton
onClick={onApprove}
invert={status === "approved"}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useRouter } from "found";
import React, {
FunctionComponent,
Reducer,
useCallback,
useEffect,
useMemo,
useReducer,
useState,
} from "react";
import { graphql } from "react-relay";
Expand Down Expand Up @@ -31,6 +34,7 @@ import { ModerateCardContainer_settings } from "coral-admin/__generated__/Modera
import { ModerateCardContainer_viewer } from "coral-admin/__generated__/ModerateCardContainer_viewer.graphql";
import { ModerateCardContainerLocal } from "coral-admin/__generated__/ModerateCardContainerLocal.graphql";
import { UserStatusChangeContainer_viewer } from "coral-admin/__generated__/UserStatusChangeContainer_viewer.graphql";
import { RejectCommentInput } from "coral-stream/__generated__/RejectCommentMutation.graphql";

import FeatureCommentMutation from "./FeatureCommentMutation";
import ModerateCard from "./ModerateCard";
Expand Down Expand Up @@ -70,6 +74,19 @@ function isFeatured(comment: ModerateCardContainer_comment) {
return comment.tags.some((t) => t.code === GQLTAG.FEATURED);
}

interface RejectionState {
showModerationReason: boolean;
rejecting: boolean;
reason?: RejectCommentInput["reason"];
}

type RejectionAction =
| { action: "INDICATE_REJECT" }
| { action: "CONFIRM_REASON"; reason: RejectCommentInput["reason"] }
| { action: "REJECTION_COMPLETE" };

type RejectionReducer = Reducer<RejectionState, RejectionAction>;

const ModerateCardContainer: FunctionComponent<Props> = ({
comment,
settings,
Expand All @@ -94,10 +111,11 @@ const ModerateCardContainer: FunctionComponent<Props> = ({

const { match, router } = useRouter();

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

Expand All @@ -109,6 +127,27 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
);

const [showBanModal, setShowBanModal] = useState(false);
const [{ rejecting, reason: rejectionReason }, dispatch] =
useReducer<RejectionReducer>( // TODO use dispatch
(state, input) => {
switch (input.action) {
case "INDICATE_REJECT":
return dsaFeaturesEnabled
? { showModerationReason: true, rejecting: false }
: { showModerationReason: false, rejecting: true };
case "CONFIRM_REASON":
return {
showModerationReason: false,
rejecting: true,
reason: input.reason,
};
case "REJECTION_COMPLETE":
return { showModerationReason: false, rejecting: false };
}
},
{ showModerationReason: false, rejecting: false }
);

const handleApprove = useCallback(async () => {
if (!comment.revision) {
return;
Expand Down Expand Up @@ -159,11 +198,15 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
await rejectComment({
commentID: comment.id,
commentRevisionID: comment.revision.id,
reason: rejectionReason,
storyID,
siteID,
section,
orderBy: moderationQueueSort,
});

dispatch({ action: "REJECTION_COMPLETE" });

if (loadNext) {
loadNext();
}
Expand All @@ -179,6 +222,7 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
loadNext,
moderationQueueSort,
onModerated,
rejectionReason,
]);

const handleFeature = useCallback(() => {
Expand Down Expand Up @@ -276,6 +320,12 @@ const ModerateCardContainer: FunctionComponent<Props> = ({

const handleBanConfirm = useCallback(() => setShowBanModal(false), []);

useEffect(() => {
if (rejecting) {
void handleReject();
}
}, [handleReject, rejecting]);

// Only highlight comments that have been flagged for containing a banned or
// suspect word.
const highlight = useMemo(() => {
Expand Down Expand Up @@ -319,7 +369,7 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
featured={isFeatured(comment)}
viewContextHref={comment.permalink}
onApprove={handleApprove}
onReject={handleReject}
onReject={() => dispatch({ action: "INDICATE_REJECT" })}
onFeature={onFeature}
onUsernameClick={onUsernameClicked}
onConversationClick={
Expand Down Expand Up @@ -357,6 +407,8 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
}
isArchived={comment.story.isArchived}
isArchiving={comment.story.isArchiving}
dsaFeaturesEnabled={!!dsaFeaturesEnabled}
onReason={(reason) => dispatch({ action: "CONFIRM_REASON", reason })}
/>
</FadeInTransition>
{comment.author && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ $moderateCardButtonOutlineRejectColor: var(--palette-error-500);
border-radius: var(--round-corners);
width: 65px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
color: $moderateCardButtonOutlineRejectColor;
&:not(:disabled):active {
background-color: $moderateCardButtonOutlineRejectColor;
color: var(--palette-text-000);
}
}

.open {
background-color: var(--palette-error-400);
color: var(--palette-text-000);
}

.readOnly {
background-color: transparent;
border-color: $colors-grey-300);
Expand All @@ -42,6 +45,17 @@ $moderateCardButtonOutlineRejectColor: var(--palette-error-500);
border-color: $moderateCardButtonOutlineRejectColor;
}

.icon {
.xIcon {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: inherit;
}

.arrowIcon {
position: absolute;
left: 75%;
top: 25%;
transform: translate(-50%, 25%);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,55 @@ import cn from "classnames";
import React, { FunctionComponent } from "react";

import { PropTypesOf } from "coral-framework/types";
import { RemoveIcon, SvgIcon } from "coral-ui/components/icons";
import {
ArrowsDownIcon,
ArrowsUpIcon,
RemoveIcon,
SvgIcon,
} from "coral-ui/components/icons";
import { BaseButton } from "coral-ui/components/v2";

import { withForwardRef } from "coral-ui/hocs";
import styles from "./RejectButton.css";

interface Props extends Omit<PropTypesOf<typeof BaseButton>, "ref"> {
toggle?: boolean;
open?: boolean;
invert?: boolean;
readOnly?: boolean;
forwardRef: React.Ref<any>;
}

const RejectButton: FunctionComponent<Props> = ({
invert,
readOnly,
className,
open = false,
toggle = false,
forwardRef,
...rest
}) => (
<Localized id="moderate-comment-rejectButton" attrs={{ "aria-label": true }}>
<BaseButton
{...rest}
ref={forwardRef}
className={cn(className, styles.root, {
[styles.open]: toggle && open,
[styles.invert]: invert,
[styles.readOnly]: readOnly,
})}
aria-label="Reject"
>
<SvgIcon size="lg" className={styles.icon} Icon={RemoveIcon} />
<SvgIcon size="lg" className={styles.xIcon} Icon={RemoveIcon} />
{toggle && (
<SvgIcon
size="xxs"
className={styles.arrowIcon}
Icon={open ? ArrowsUpIcon : ArrowsDownIcon}
/>
)}
</BaseButton>
</Localized>
);

export default RejectButton;
export default withForwardRef(RejectButton);
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.detailedExplanation {
margin-bottom: var(--spacing-3);
}

.explanationLabel {
margin-bottom: var(--spacing-2);
}

.changeReason {
margin-bottom: var(--spacing-3);
}

.detailedExplanation > textarea {
resize: none;
}
Loading

0 comments on commit b18e52c

Please sign in to comment.