From e77103d87243d4c8934e639f7102e0680ea442f7 Mon Sep 17 00:00:00 2001 From: Nick Funk Date: Fri, 28 Jun 2019 17:10:09 -0600 Subject: [PATCH] [CORL-382] Allow moderators to ban a commenter from the comment stream (#2378) * Add a ban user action to the stream moderation drop down CORL-382 * Show banned stated for user in stream moderation drop down If the user is actively banned, the ban user option will be disabled and show a status of "banned". CORL-382 * Create utility for generating random stories, comments, and users CORL-382 * Add ban and suspension values to baseUser in fixtures CORL-382 * Updated banned.spec.tsx to use new test utilities for generating fixture data CORL-382 * Prevent users from being able to ban themselves in the moderation dropdown CORL-382 * Kill optimistic response errors for comment mutations Set the author.status.current to an empty array so it stops complaining about it being unused. CORL-382 * Rename util in tests to helpers/fixture.ts CORL-382 * Remove unused import from CreateCommentReplyMutation.ts CORL-382 * Put back the optimistic ban responses into comment mutations The warnings spewing out during tests are false, for further detail please see: https://github.com/facebook/relay/pull/2760 CORL-382 * Denormalize generated stories and comments CORL-382 * Clean up import ordering in ModerationActionsContainer.tsx CORL-382 * Inject appropriate scoped items into callbacks for moderation dropdown CORL-382 * Set optimistic response author status from known viewer status CORL-382 --- .../Comments/Comment/CommentContainer.tsx | 7 +- .../ModerationDropdown/CaretContainer.tsx | 11 +- ...ner.css => ModerationActionsContainer.css} | 10 + .../ModerationActionsContainer.tsx | 223 ++++++++++++++++++ .../ModerationDropdownContainer.tsx | 169 ++++--------- .../CreateCommentReplyMutation.ts | 6 + .../Comment/UserBanPopover/BanUserMutation.ts | 62 +++++ .../UserBanPopoverContainer.css | 4 + .../UserBanPopoverContainer.tsx | 71 ++++++ .../PostCommentForm/CreateCommentMutation.ts | 6 + .../test/comments/stream/banned.spec.tsx | 28 ++- src/core/client/stream/test/fixtures.ts | 9 + .../client/stream/test/helpers/fixture.ts | 129 ++++++++++ src/locales/en-US/stream.ftl | 9 + 14 files changed, 602 insertions(+), 142 deletions(-) rename src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/{ModerationDropdownContainer.css => ModerationActionsContainer.css} (61%) create mode 100644 src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationActionsContainer.tsx create mode 100644 src/core/client/stream/tabs/Comments/Comment/UserBanPopover/BanUserMutation.ts create mode 100644 src/core/client/stream/tabs/Comments/Comment/UserBanPopover/UserBanPopoverContainer.css create mode 100644 src/core/client/stream/tabs/Comments/Comment/UserBanPopover/UserBanPopoverContainer.tsx create mode 100644 src/core/client/stream/test/helpers/fixture.ts diff --git a/src/core/client/stream/tabs/Comments/Comment/CommentContainer.tsx b/src/core/client/stream/tabs/Comments/Comment/CommentContainer.tsx index 476932000..925ac3fcc 100644 --- a/src/core/client/stream/tabs/Comments/Comment/CommentContainer.tsx +++ b/src/core/client/stream/tabs/Comments/Comment/CommentContainer.tsx @@ -243,7 +243,11 @@ export class CommentContainer extends Component { )} {showCaret && ( - + )} } @@ -329,6 +333,7 @@ const enhanced = withSetCommentIDMutation( ...UsernameWithPopoverContainer_viewer ...ReactionButtonContainer_viewer ...ReportButtonContainer_viewer + ...CaretContainer_viewer } `, story: graphql` diff --git a/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/CaretContainer.tsx b/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/CaretContainer.tsx index b244f5310..ee1a91a03 100644 --- a/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/CaretContainer.tsx +++ b/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/CaretContainer.tsx @@ -5,6 +5,7 @@ import { graphql } from "react-relay"; import { withFragmentContainer } from "coral-framework/lib/relay"; import { CaretContainer_comment } from "coral-stream/__generated__/CaretContainer_comment.graphql"; import { CaretContainer_story } from "coral-stream/__generated__/CaretContainer_story.graphql"; +import { CaretContainer_viewer } from "coral-stream/__generated__/CaretContainer_viewer.graphql"; import { Button, ClickOutside, Icon, Popover } from "coral-ui/components"; import ModerationDropdownContainer from "./ModerationDropdownContainer"; @@ -14,6 +15,7 @@ import styles from "./CaretContainer.css"; interface Props { comment: CaretContainer_comment; story: CaretContainer_story; + viewer: CaretContainer_viewer | null; } const CaretContainer: FunctionComponent = props => { @@ -27,12 +29,14 @@ const CaretContainer: FunctionComponent = props => { id={popoverID} placement="bottom-end" description="A popover menu to moderate the comment" - body={({ toggleVisibility }) => ( + body={({ toggleVisibility, scheduleUpdate }) => ( )} @@ -72,6 +76,11 @@ const enhanced = withFragmentContainer({ ...ModerationDropdownContainer_story } `, + viewer: graphql` + fragment CaretContainer_viewer on User { + ...ModerationDropdownContainer_viewer + } + `, })(CaretContainer); export default enhanced; diff --git a/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationDropdownContainer.css b/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationActionsContainer.css similarity index 61% rename from src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationDropdownContainer.css rename to src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationActionsContainer.css index 7662868a2..bbd380b84 100644 --- a/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationDropdownContainer.css +++ b/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationActionsContainer.css @@ -10,3 +10,13 @@ color: var(--palette-primary-dark); font-weight: var(--font-weight-bold); } +.banned { + color: var(--palette-error-dark); + font-weight: var(--font-weight-bold); +} +.banIcon { + width: 18px; + height: 14px; + text-align: center; + line-height: 12px; +} \ No newline at end of file diff --git a/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationActionsContainer.tsx b/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationActionsContainer.tsx new file mode 100644 index 000000000..75ccd58c2 --- /dev/null +++ b/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationActionsContainer.tsx @@ -0,0 +1,223 @@ +import cn from "classnames"; +import { Localized } from "fluent-react/compat"; +import React, { FunctionComponent, useCallback } from "react"; +import { graphql } from "react-relay"; + +import { useMutation, withFragmentContainer } from "coral-framework/lib/relay"; +import { ModerationActionsContainer_comment } from "coral-stream/__generated__/ModerationActionsContainer_comment.graphql"; +import { ModerationActionsContainer_story } from "coral-stream/__generated__/ModerationActionsContainer_story.graphql"; +import { ModerationActionsContainer_viewer } from "coral-stream/__generated__/ModerationActionsContainer_viewer.graphql"; +import { DropdownButton, DropdownDivider, Icon } from "coral-ui/components"; + +import ApproveCommentMutation from "./ApproveCommentMutation"; +import FeatureCommentMutation from "./FeatureCommentMutation"; +import RejectCommentMutation from "./RejectCommentMutation"; +import UnfeatureCommentMutation from "./UnfeatureCommentMutation"; + +import styles from "./ModerationActionsContainer.css"; + +interface Props { + comment: ModerationActionsContainer_comment; + story: ModerationActionsContainer_story; + viewer: ModerationActionsContainer_viewer | null; + onDismiss: () => void; + onBan: () => void; +} + +const ModerationActionsContainer: FunctionComponent = ({ + comment, + story, + viewer, + onDismiss, + onBan, +}) => { + const approve = useMutation(ApproveCommentMutation); + const feature = useMutation(FeatureCommentMutation); + const unfeature = useMutation(UnfeatureCommentMutation); + const reject = useMutation(RejectCommentMutation); + + const onApprove = useCallback(() => { + approve({ commentID: comment.id, commentRevisionID: comment.revision.id }); + }, [approve, comment]); + const onReject = useCallback( + () => + reject({ commentID: comment.id, commentRevisionID: comment.revision.id }), + [approve, comment] + ); + const onFeature = useCallback(() => { + feature({ + storyID: story.id, + commentID: comment.id, + commentRevisionID: comment.revision.id, + }); + onDismiss(); + }, [feature, onDismiss, story, comment]); + const onUnfeature = useCallback(() => { + unfeature({ + commentID: comment.id, + storyID: story.id, + }); + onDismiss(); + }, [unfeature, onDismiss, story, comment]); + const approved = comment.status === "APPROVED"; + const rejected = comment.status === "REJECTED"; + const featured = comment.tags.some(t => t.code === "FEATURED"); + const banned = comment.author!.status.ban.active; + const showBanOption = + viewer === null ? false : comment.author!.id !== viewer.id; + + return ( + <> + {featured ? ( + + + star + + } + className={styles.featured} + onClick={onUnfeature} + > + Un-Feature + + + ) : ( + + star_border} + onClick={onFeature} + > + Feature + + + )} + {approved ? ( + + + check + + } + className={styles.approved} + disabled + > + Approved + + + ) : ( + + check} + onClick={onApprove} + > + Approve + + + )} + {rejected ? ( + + + close + + } + className={styles.rejected} + disabled + > + Rejected + + + ) : ( + + close} + onClick={onReject} + > + Reject + + + )} + {showBanOption && ( + <> + + {banned ? ( + + + block + + } + className={styles.banned} + disabled + > + Banned + + + ) : ( + + + block + + } + onClick={onBan} + > + Ban User + + + )} + + )} + + + + Go to Moderate + + + + ); +}; + +const enhanced = withFragmentContainer({ + comment: graphql` + fragment ModerationActionsContainer_comment on Comment { + id + author { + id + status { + ban { + active + } + } + } + revision { + id + } + status + tags { + code + } + } + `, + story: graphql` + fragment ModerationActionsContainer_story on Story { + id + } + `, + viewer: graphql` + fragment ModerationActionsContainer_viewer on User { + id + } + `, +})(ModerationActionsContainer); + +export default enhanced; diff --git a/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationDropdownContainer.tsx b/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationDropdownContainer.tsx index f83850f6e..0c60a7f77 100644 --- a/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationDropdownContainer.tsx +++ b/src/core/client/stream/tabs/Comments/Comment/ModerationDropdown/ModerationDropdownContainer.tsx @@ -1,153 +1,54 @@ -import { Localized } from "fluent-react/compat"; -import React, { FunctionComponent, useCallback } from "react"; +import React, { FunctionComponent, useCallback, useState } from "react"; import { graphql } from "react-relay"; -import { useMutation, withFragmentContainer } from "coral-framework/lib/relay"; +import { withFragmentContainer } from "coral-framework/lib/relay"; import { ModerationDropdownContainer_comment } from "coral-stream/__generated__/ModerationDropdownContainer_comment.graphql"; import { ModerationDropdownContainer_story } from "coral-stream/__generated__/ModerationDropdownContainer_story.graphql"; -import { - Dropdown, - DropdownButton, - DropdownDivider, - Icon, -} from "coral-ui/components"; +import { ModerationDropdownContainer_viewer } from "coral-stream/__generated__/ModerationDropdownContainer_viewer.graphql"; +import { Dropdown } from "coral-ui/components"; -import ApproveCommentMutation from "./ApproveCommentMutation"; -import FeatureCommentMutation from "./FeatureCommentMutation"; -import RejectCommentMutation from "./RejectCommentMutation"; -import UnfeatureCommentMutation from "./UnfeatureCommentMutation"; +import UserBanPopoverContainer from "../UserBanPopover/UserBanPopoverContainer"; +import ModerationActionsContainer from "./ModerationActionsContainer"; -import styles from "./ModerationDropdownContainer.css"; +type View = "MODERATE" | "BAN"; interface Props { comment: ModerationDropdownContainer_comment; story: ModerationDropdownContainer_story; + viewer: ModerationDropdownContainer_viewer | null; onDismiss: () => void; + scheduleUpdate: () => void; } const ModerationDropdownContainer: FunctionComponent = ({ comment, story, + viewer, onDismiss, + scheduleUpdate, }) => { - const approve = useMutation(ApproveCommentMutation); - const feature = useMutation(FeatureCommentMutation); - const unfeature = useMutation(UnfeatureCommentMutation); - const reject = useMutation(RejectCommentMutation); - - const onApprove = useCallback(() => { - approve({ commentID: comment.id, commentRevisionID: comment.revision.id }); - }, [approve, comment]); - const onReject = useCallback( - () => - reject({ commentID: comment.id, commentRevisionID: comment.revision.id }), - [approve, comment] - ); - const onFeature = useCallback(() => { - feature({ - storyID: story.id, - commentID: comment.id, - commentRevisionID: comment.revision.id, - }); - onDismiss(); - }, [feature, comment]); - const onUnfeature = useCallback(() => { - unfeature({ - commentID: comment.id, - storyID: story.id, - }); - onDismiss(); - }, [unfeature, comment]); - - const approved = comment.status === "APPROVED"; - const rejected = comment.status === "REJECTED"; - const featured = comment.tags.some(t => t.code === "FEATURED"); + const [view, setView] = useState("MODERATE"); + const onBan = useCallback(() => { + setView("BAN"); + scheduleUpdate(); + }, [setView, scheduleUpdate]); return ( - - {featured ? ( - - - star - - } - className={styles.featured} - onClick={onUnfeature} - > - Un-Feature - - +
+ {view === "MODERATE" ? ( + + + ) : ( - - star_border} - onClick={onFeature} - > - Feature - - + )} - {approved ? ( - - - check - - } - className={styles.approved} - disabled - > - Approved - - - ) : ( - - check} - onClick={onApprove} - > - Approve - - - )} - {rejected ? ( - - - close - - } - className={styles.rejected} - disabled - > - Rejected - - - ) : ( - - close} - onClick={onReject} - > - Reject - - - )} - - - - Go to Moderate - - - +
); }; @@ -155,6 +56,11 @@ const enhanced = withFragmentContainer({ comment: graphql` fragment ModerationDropdownContainer_comment on Comment { id + author { + id + username + ...UserBanPopoverContainer_user + } revision { id } @@ -162,11 +68,18 @@ const enhanced = withFragmentContainer({ tags { code } + ...ModerationActionsContainer_comment } `, story: graphql` fragment ModerationDropdownContainer_story on Story { id + ...ModerationActionsContainer_story + } + `, + viewer: graphql` + fragment ModerationDropdownContainer_viewer on User { + ...ModerationActionsContainer_viewer } `, })(ModerationDropdownContainer); diff --git a/src/core/client/stream/tabs/Comments/Comment/ReplyCommentForm/CreateCommentReplyMutation.ts b/src/core/client/stream/tabs/Comments/Comment/ReplyCommentForm/CreateCommentReplyMutation.ts index 14fbe2c83..3c4ba5ed8 100644 --- a/src/core/client/stream/tabs/Comments/Comment/ReplyCommentForm/CreateCommentReplyMutation.ts +++ b/src/core/client/stream/tabs/Comments/Comment/ReplyCommentForm/CreateCommentReplyMutation.ts @@ -179,6 +179,12 @@ function commit( username: viewer.username, createdAt: viewer.createdAt, ignoreable: false, + status: { + current: viewer.status.current, + ban: { + active: viewer.status.ban.active, + }, + }, }, body: input.body, revision: { diff --git a/src/core/client/stream/tabs/Comments/Comment/UserBanPopover/BanUserMutation.ts b/src/core/client/stream/tabs/Comments/Comment/UserBanPopover/BanUserMutation.ts new file mode 100644 index 000000000..66af746ab --- /dev/null +++ b/src/core/client/stream/tabs/Comments/Comment/UserBanPopover/BanUserMutation.ts @@ -0,0 +1,62 @@ +import { graphql } from "react-relay"; +import { Environment } from "relay-runtime"; + +import { + commitMutationPromiseNormalized, + createMutation, + lookup, + MutationInput, +} from "coral-framework/lib/relay"; +import { GQLUser, GQLUSER_STATUS } from "coral-framework/schema"; +import { BanUserMutation } from "coral-stream/__generated__/BanUserMutation.graphql"; + +let clientMutationId = 0; + +const BanUserMutation = createMutation( + "banUser", + (environment: Environment, input: MutationInput) => { + return commitMutationPromiseNormalized(environment, { + mutation: graphql` + mutation BanUserMutation($input: BanUserInput!) { + banUser(input: $input) { + user { + id + status { + current + ban { + active + } + } + } + clientMutationId + } + } + `, + variables: { + input: { + ...input, + clientMutationId: clientMutationId.toString(), + }, + }, + optimisticResponse: { + banUser: { + user: { + id: input.userID, + status: { + current: lookup( + environment, + input.userID + )!.status.current.concat(GQLUSER_STATUS.BANNED), + ban: { + active: true, + }, + }, + }, + clientMutationId: (clientMutationId++).toString(), + }, + }, + }); + } +); + +export default BanUserMutation; diff --git a/src/core/client/stream/tabs/Comments/Comment/UserBanPopover/UserBanPopoverContainer.css b/src/core/client/stream/tabs/Comments/Comment/UserBanPopover/UserBanPopoverContainer.css new file mode 100644 index 000000000..362844dbd --- /dev/null +++ b/src/core/client/stream/tabs/Comments/Comment/UserBanPopover/UserBanPopoverContainer.css @@ -0,0 +1,4 @@ +.root { + width: 280px; + max-width: 80vw; +} \ No newline at end of file diff --git a/src/core/client/stream/tabs/Comments/Comment/UserBanPopover/UserBanPopoverContainer.tsx b/src/core/client/stream/tabs/Comments/Comment/UserBanPopover/UserBanPopoverContainer.tsx new file mode 100644 index 000000000..889af23f0 --- /dev/null +++ b/src/core/client/stream/tabs/Comments/Comment/UserBanPopover/UserBanPopoverContainer.tsx @@ -0,0 +1,71 @@ +import { Localized } from "fluent-react/compat"; +import React, { FunctionComponent, useCallback } from "react"; +import { graphql } from "react-relay"; + +import { useMutation, withFragmentContainer } from "coral-framework/lib/relay"; +import { UserBanPopoverContainer_user as UserData } from "coral-stream/__generated__/UserBanPopoverContainer_user.graphql"; +import { Box, Button, Flex, Typography } from "coral-ui/components"; + +import BanUserMutation from "./BanUserMutation"; + +import styles from "./UserBanPopoverContainer.css"; + +interface Props { + onDismiss: () => void; + user: UserData; +} + +const UserBanPopoverContainer: FunctionComponent = ({ + user, + onDismiss, +}) => { + const banUser = useMutation(BanUserMutation); + const onBan = useCallback(() => { + banUser({ userID: user.id }); + onDismiss(); + }, [user, banUser, onDismiss]); + return ( + + + + Ban {user.username}? + + + + + Once banned, this user will no longer be able to comment, use + reactions, or report comments. + + + + + + + + + + + + ); +}; + +const enhanced = withFragmentContainer({ + user: graphql` + fragment UserBanPopoverContainer_user on User { + id + username + status { + current + ban { + active + } + } + } + `, +})(UserBanPopoverContainer); + +export default enhanced; diff --git a/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/CreateCommentMutation.ts b/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/CreateCommentMutation.ts index 802566c27..6efa2b577 100644 --- a/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/CreateCommentMutation.ts +++ b/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/CreateCommentMutation.ts @@ -149,6 +149,12 @@ function commit( username: viewer.username, createdAt: viewer.createdAt, ignoreable: false, + status: { + current: viewer.status.current, + ban: { + active: viewer.status.ban.active, + }, + }, }, revision: { id: uuidGenerator(), diff --git a/src/core/client/stream/test/comments/stream/banned.spec.tsx b/src/core/client/stream/test/comments/stream/banned.spec.tsx index 6746c5d07..38e6ae6d3 100644 --- a/src/core/client/stream/test/comments/stream/banned.spec.tsx +++ b/src/core/client/stream/test/comments/stream/banned.spec.tsx @@ -1,20 +1,29 @@ import timekeeper from "timekeeper"; import { pureMerge } from "coral-common/utils"; -import { GQLResolver, GQLUSER_STATUS } from "coral-framework/schema"; +import { GQLResolver } from "coral-framework/schema"; import { createResolversStub, CreateTestRendererParams, waitForElement, within, } from "coral-framework/testHelpers"; +import { + createComment, + createStory, + createUser, + createUserStatus, +} from "coral-stream/test/helpers/fixture"; -import { comments, settings, stories } from "../../fixtures"; +import { settings } from "../../fixtures"; import create from "./create"; -const story = stories[0]; +const bannedUser = createUser(); +bannedUser.status = createUserStatus(true); + +const story = createStory(); const firstComment = story.comments.edges[0].node; -const viewer = firstComment.author!; +const reactedComment = createComment(); async function createTestRenderer( params: CreateTestRendererParams = {} @@ -25,22 +34,17 @@ async function createTestRenderer( createResolversStub({ Query: { settings: () => settings, - viewer: () => - pureMerge(viewer, { - status: { - current: [GQLUSER_STATUS.BANNED], - }, - }), + viewer: () => bannedUser, story: () => pureMerge(story, { comments: { edges: [ ...story.comments.edges, { - node: pureMerge(comments[2], { + node: pureMerge(reactedComment, { actionCounts: { reaction: { total: 1 } }, }), - cursor: comments[2].createdAt, + cursor: reactedComment.createdAt, }, ], }, diff --git a/src/core/client/stream/test/fixtures.ts b/src/core/client/stream/test/fixtures.ts index 6221c9e1b..cc0a5abe9 100644 --- a/src/core/client/stream/test/fixtures.ts +++ b/src/core/client/stream/test/fixtures.ts @@ -90,6 +90,15 @@ export const baseUser = createFixture({ createdAt: "2018-02-06T18:24:00.000Z", status: { current: [GQLUSER_STATUS.ACTIVE], + ban: { + active: false, + history: [], + }, + suspension: { + active: false, + until: null, + history: [], + }, }, ignoredUsers: [], comments: { diff --git a/src/core/client/stream/test/helpers/fixture.ts b/src/core/client/stream/test/helpers/fixture.ts new file mode 100644 index 000000000..05dda4cf2 --- /dev/null +++ b/src/core/client/stream/test/helpers/fixture.ts @@ -0,0 +1,129 @@ +import { + GQLComment, + GQLCOMMENT_STATUS, + GQLMODERATION_MODE, + GQLStory, + GQLUser, + GQLUSER_ROLE, + GQLUSER_STATUS, +} from "coral-framework/schema"; +import { + createFixture, + denormalizeComment, + denormalizeStory, +} from "coral-framework/testHelpers"; +import uuid from "uuid/v4"; + +export function createDateInRange(start: Date, end: Date) { + return new Date( + start.getTime() + Math.random() * (end.getTime() - start.getTime()) + ); +} + +export function randomDate() { + return createDateInRange(new Date(2000, 0, 1), new Date()); +} + +export function createUserStatus(banned: boolean = false) { + return { + current: [banned ? GQLUSER_STATUS.BANNED : GQLUSER_STATUS.ACTIVE], + ban: { + active: banned, + history: [], + }, + suspension: { + active: false, + until: null, + history: [], + }, + }; +} + +export function createUser() { + return createFixture({ + id: uuid(), + username: uuid(), + role: GQLUSER_ROLE.COMMENTER, + createdAt: randomDate().toISOString(), + status: createUserStatus(), + ignoredUsers: [], + comments: { + edges: [], + pageInfo: { + hasNextPage: false, + }, + }, + ignoreable: true, + }); +} + +export function createComment() { + const revision = uuid(); + const createdAt = randomDate(); + const editableUntil = new Date(createdAt.getTime() + 30 * 60000); + const author = createUser(); + author.createdAt = new Date(createdAt.getTime() - 60 * 60000).toISOString(); + + return denormalizeComment( + createFixture({ + id: uuid(), + author, + body: uuid(), + revision: { + id: revision, + }, + status: GQLCOMMENT_STATUS.NONE, + createdAt: createdAt.toISOString(), + replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } }, + replyCount: 0, + editing: { + edited: false, + editableUntil: editableUntil.toISOString(), + }, + actionCounts: { + reaction: { + total: 0, + }, + }, + tags: [], + }) + ); +} + +export function createStory(createComments: boolean = true) { + const id = uuid(); + const comments = [createComment(), createComment()]; + + return denormalizeStory( + createFixture({ + id, + url: `http://localhost/stories/story-${id}`, + comments: { + edges: [ + { node: comments[0], cursor: comments[0].createdAt }, + { node: comments[1], cursor: comments[1].createdAt }, + ], + pageInfo: { + hasNextPage: false, + }, + }, + metadata: { + title: uuid(), + }, + isClosed: false, + commentCounts: { + totalVisible: 0, + tags: { + FEATURED: 0, + }, + }, + settings: { + moderation: GQLMODERATION_MODE.POST, + premodLinksEnable: false, + messageBox: { + enabled: false, + }, + }, + }) + ); +} diff --git a/src/locales/en-US/stream.ftl b/src/locales/en-US/stream.ftl index ff3c94d2b..27caa32ac 100644 --- a/src/locales/en-US/stream.ftl +++ b/src/locales/en-US/stream.ftl @@ -111,6 +111,13 @@ comments-userIgnorePopover-description = comments-userIgnorePopover-ignore = Ignore comments-userIgnorePopover-cancel = Cancel +comments-userBanPopover-title = Ban {$username}? +comments-userBanPopover-description = + Once banned, this user will no longer be able + to comment, use reactions, or report comments. +comments-userBanPopover-cancel = Cancel +comments-userBanPopover-ban = Ban + comments-moderationDropdown-popover = .description = A popover menu to moderate the comment comments-moderationDropdown-feature = Feature @@ -119,6 +126,8 @@ comments-moderationDropdown-approve = Approve comments-moderationDropdown-approved = Approved comments-moderationDropdown-reject = Reject comments-moderationDropdown-rejected = Rejected +comments-moderationDropdown-ban = Ban User +comments-moderationDropdown-banned = Banned comments-moderationDropdown-goToModerate = Go to Moderate comments-moderationDropdown-caretButton = .aria-label = Moderate