[CORL-224] Flag Details (#2293)

* feat: flag details

* fix: review

* feat: implement flag details in admin

* test: test flag details
This commit is contained in:
Wyatt Johnson
2019-05-15 18:50:47 +02:00
committed by Kiwi
parent 898bc0de06
commit 3ba10fd15f
23 changed files with 1259 additions and 214 deletions
@@ -0,0 +1,3 @@
.category {
color: var(--palette-error-darkest);
}
@@ -0,0 +1,26 @@
import React, { FunctionComponent } from "react";
import { HorizontalGutter, Typography } from "talk-ui/components";
import styles from "./FlagDetailsCategory.css";
interface Props {
category: React.ReactNode;
children?: React.ReactNode;
}
const FlagDetailsCategory: FunctionComponent<Props> = ({
category,
children,
}) => {
return (
<HorizontalGutter size="half">
<Typography variant="bodyCopyBold" className={styles.category}>
{category}
</Typography>
<HorizontalGutter size="half">{children}</HorizontalGutter>
</HorizontalGutter>
);
};
export default FlagDetailsCategory;
@@ -0,0 +1,18 @@
.user {
font-size: calc(14rem / var(--rem-base));
font-weight: var(--font-weight-medium);
font-family: var(--font-family-serif);
line-height: calc(16em / 14);
letter-spacing: calc(0.2em / 14);
color: var(--palette-text-primary);
padding-right: calc(0.5 * var(--mini-unit));
}
.details {
font-size: calc(14rem / var(--rem-base));
font-weight: var(--font-weight-regular);
font-family: var(--font-family-sans-serif);
line-height: calc(16em / 14);
letter-spacing: calc(0.2em / 14);
color: var(--palette-text-primary);
font-style: italic;
}
@@ -0,0 +1,22 @@
import React, { FunctionComponent } from "react";
import styles from "./FlagDetailsEntry.css";
interface Props {
user: React.ReactNode;
details?: React.ReactNode;
}
const FlagDetailsEntry: FunctionComponent<Props> = ({ user, details }) => {
return (
<div>
<span className={styles.user}>
{user}
{details && ":"}
</span>
{details && <span className={styles.details}>{details}</span>}
</div>
);
};
export default FlagDetailsEntry;
@@ -0,0 +1,16 @@
.detailsButton {
height: 100%;
padding: 2px 0px;
margin-left: var(--mini-unit);
font-size: calc(12rem / var(--rem-base));
letter-spacing: calc(-0.1em / 12);
align-self: flex-end;
text-transform: uppercase;
&.detailsButtonColorRegular {
color: var(--palette-text-primary);
}
}
.detailsText {
margin: 0;
}
@@ -0,0 +1,47 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent, useCallback, useState } from "react";
import { useUUID } from "talk-framework/hooks";
import { Button, Flex, HorizontalGutter, Icon } from "talk-ui/components";
import styles from "./Markers.css";
interface Props {
children: React.ReactNode;
details: React.ReactElement | null;
}
const Markers: FunctionComponent<Props> = ({ children, details }) => {
const uuid = useUUID();
const [showDetails, setShowDetails] = useState<boolean>(false);
const toggleDetails = useCallback(() => setShowDetails(!showDetails), [
showDetails,
]);
return (
<HorizontalGutter size="double">
<Flex>
<Flex itemGutter>{children}</Flex>
{details && (
<Button
size="small"
classes={{
variantRegular: styles.detailsButton,
colorRegular: styles.detailsButtonColorRegular,
}}
onClick={toggleDetails}
aria-controls={uuid}
aria-expanded={showDetails}
>
<Localized id="moderate-markers-details">
<span className={styles.detailsText}>DETAILS</span>
</Localized>
<Icon>{showDetails ? "arrow_drop_up" : "arrow_drop_down"}</Icon>
</Button>
)}
</Flex>
{showDetails && <div id={uuid}>{details}</div>}
</HorizontalGutter>
);
};
export default Markers;
@@ -0,0 +1,93 @@
import { Localized } from "fluent-react/compat";
import React from "react";
import { graphql } from "react-relay";
import { FlagDetailsContainer_comment as CommentData } from "talk-admin/__generated__/FlagDetailsContainer_comment.graphql";
import NotAvailable from "talk-admin/components/NotAvailable";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { GQLCOMMENT_FLAG_REASON } from "talk-framework/schema";
import { HorizontalGutter } from "talk-ui/components";
import FlagDetailsCategory from "../components/FlagDetailsCategory";
import FlagDetailsEntry from "../components/FlagDetailsEntry";
interface Props {
comment: CommentData;
}
export class FlagDetailsContainer extends React.Component<Props> {
public render() {
const nodes = this.props.comment.flags.nodes;
const offensiveList: React.ReactElement[] = [];
const spamList: React.ReactElement[] = [];
nodes.forEach(n => {
switch (n.reason) {
case GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_OFFENSIVE:
offensiveList.push(
<FlagDetailsEntry
user={n.flagger ? n.flagger.username : <NotAvailable />}
details={n.additionalDetails}
/>
);
return;
case GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_SPAM:
spamList.push(
<FlagDetailsEntry
user={n.flagger ? n.flagger.username : <NotAvailable />}
details={n.additionalDetails}
/>
);
return;
default:
return;
}
});
if (offensiveList.length + spamList.length === 0) {
return null;
}
return (
<HorizontalGutter size="oneAndAHalf">
{offensiveList.length > 0 && (
<FlagDetailsCategory
category={
<Localized id="moderate-flagDetails-offensive">
<span>Offensive</span>
</Localized>
}
>
{offensiveList}
</FlagDetailsCategory>
)}
{spamList.length > 0 && (
<FlagDetailsCategory
category={
<Localized id="moderate-flagDetails-span">
<span>Spam</span>
</Localized>
}
>
{spamList}
</FlagDetailsCategory>
)}
</HorizontalGutter>
);
}
}
const enhanced = withFragmentContainer<Props>({
comment: graphql`
fragment FlagDetailsContainer_comment on Comment {
flags {
nodes {
flagger {
username
}
reason
additionalDetails
}
}
}
`,
})(FlagDetailsContainer);
export default enhanced;
@@ -4,12 +4,22 @@ import { graphql } from "react-relay";
import { MarkersContainer_comment as CommentData } from "talk-admin/__generated__/MarkersContainer_comment.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { Flex, Marker, MarkerCount } from "talk-ui/components";
import { Marker, MarkerCount } from "talk-ui/components";
import Markers from "../components/Markers";
import FlagDetailsContainer from "./FlagDetailsContainer";
interface MarkersContainerProps {
comment: CommentData;
}
function hasDetails(c: CommentData) {
return (
c.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE +
c.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM >
0
);
}
let keyCounter = 0;
const markers: Array<(c: CommentData) => React.ReactElement<any> | null> = [
c =>
@@ -93,7 +103,17 @@ export class MarkersContainer extends React.Component<MarkersContainerProps> {
public render() {
const elements = markers.map(cb => cb(this.props.comment)).filter(m => m);
if (elements.length) {
return <Flex itemGutter>{elements}</Flex>;
return (
<Markers
details={
hasDetails(this.props.comment) ? (
<FlagDetailsContainer comment={this.props.comment} />
) : null
}
>
{elements}
</Markers>
);
}
return null;
}
@@ -102,6 +122,7 @@ export class MarkersContainer extends React.Component<MarkersContainerProps> {
const enhanced = withFragmentContainer<MarkersContainerProps>({
comment: graphql`
fragment MarkersContainer_comment on Comment {
...FlagDetailsContainer_comment
status
actionCounts {
flag {
@@ -1,8 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders all markers 1`] = `
<ForwardRef(forwardRef)
itemGutter={true}
<Markers
details={
<Relay(FlagDetailsContainer)
comment={
Object {
"actionCounts": Object {
"flag": Object {
"reasons": Object {
"COMMENT_DETECTED_BANNED_WORD": 1,
"COMMENT_DETECTED_LINKS": 1,
"COMMENT_DETECTED_SPAM": 1,
"COMMENT_DETECTED_SUSPECT_WORD": 1,
"COMMENT_DETECTED_TOXIC": 1,
"COMMENT_DETECTED_TRUST": 1,
"COMMENT_REPORTED_OFFENSIVE": 2,
"COMMENT_REPORTED_SPAM": 3,
},
},
},
"status": "PREMOD",
}
}
/>
}
>
<Localized
id="moderate-marker-preMod"
@@ -98,12 +120,34 @@ exports[`renders all markers 1`] = `
3
</withPropsOnChange(Count)>
</withPropsOnChange(Marker)>
</ForwardRef(forwardRef)>
</Markers>
`;
exports[`renders some markers 1`] = `
<ForwardRef(forwardRef)
itemGutter={true}
<Markers
details={
<Relay(FlagDetailsContainer)
comment={
Object {
"actionCounts": Object {
"flag": Object {
"reasons": Object {
"COMMENT_DETECTED_BANNED_WORD": 1,
"COMMENT_DETECTED_LINKS": 0,
"COMMENT_DETECTED_SPAM": 0,
"COMMENT_DETECTED_SUSPECT_WORD": 0,
"COMMENT_DETECTED_TOXIC": 1,
"COMMENT_DETECTED_TRUST": 1,
"COMMENT_REPORTED_OFFENSIVE": 2,
"COMMENT_REPORTED_SPAM": 0,
},
},
},
"status": "PREMOD",
}
}
/>
}
>
<Localized
id="moderate-marker-preMod"
@@ -156,5 +200,5 @@ exports[`renders some markers 1`] = `
2
</withPropsOnChange(Count)>
</withPropsOnChange(Marker)>
</ForwardRef(forwardRef)>
</Markers>
`;
+51
View File
@@ -1,5 +1,6 @@
import {
GQLComment,
GQLCOMMENT_FLAG_REASON,
GQLCOMMENT_STATUS,
GQLCommentModerationAction,
GQLCommentsConnection,
@@ -424,6 +425,9 @@ export const baseComment = createFixture<GQLComment>({
},
},
},
flags: {
nodes: [],
},
story: stories[0],
});
@@ -445,6 +449,20 @@ export const reportedComments = createFixtures<GQLComment>(
},
},
},
flags: {
nodes: [
{
reason: GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_SPAM,
flagger: users.commenters[0],
additionalDetails: "This looks like an ad",
},
{
reason: GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_SPAM,
flagger: users.commenters[1],
additionalDetails: "",
},
],
},
},
{
id: "comment-1",
@@ -461,6 +479,25 @@ export const reportedComments = createFixtures<GQLComment>(
},
},
},
flags: {
nodes: [
{
reason: GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_OFFENSIVE,
flagger: users.commenters[0],
additionalDetails: "I find this offensive",
},
{
reason: GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_OFFENSIVE,
flagger: users.commenters[1],
additionalDetails: "Not like that",
},
{
reason: GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_OFFENSIVE,
flagger: users.commenters[2],
additionalDetails: "",
},
],
},
},
{
id: "comment-2",
@@ -479,6 +516,20 @@ export const reportedComments = createFixtures<GQLComment>(
},
},
},
flags: {
nodes: [
{
reason: GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_OFFENSIVE,
flagger: users.commenters[0],
additionalDetails: "I find this offensive",
},
{
reason: GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_SPAM,
flagger: users.commenters[2],
additionalDetails: "",
},
],
},
},
],
baseComment
@@ -94,21 +94,53 @@ exports[`rejected queue accepts comment in rejected queue: dangling 1`] = `
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Spam
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
2
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -262,21 +294,53 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Spam
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
2
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -417,21 +481,53 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Offensive
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
3
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Count-root"
>
3
</span>
</span>
</div>
<button
aria-controls="uuid-1"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -578,34 +674,66 @@ exports[`rejected queue renders rejected queue with comments and load more 1`] =
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Offensive
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
1
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Count-root"
>
1
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
1
</span>
</span>
</div>
<button
aria-controls="uuid-2"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
1
</span>
</span>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -762,21 +890,53 @@ exports[`reported queue accepts comment in reported queue: dangling 1`] = `
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Spam
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
2
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -933,21 +1093,53 @@ exports[`reported queue rejects comment in reported queue: dangling 1`] = `
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Spam
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
2
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -1122,21 +1314,53 @@ exports[`reported queue renders reported queue with comments 1`] = `
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Spam
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
2
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -1277,21 +1501,53 @@ exports[`reported queue renders reported queue with comments 1`] = `
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Offensive
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
3
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Count-root"
>
3
</span>
</span>
</div>
<button
aria-controls="uuid-1"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -1448,21 +1704,53 @@ exports[`reported queue renders reported queue with comments 2`] = `
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Spam
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
2
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -1603,21 +1891,53 @@ exports[`reported queue renders reported queue with comments 2`] = `
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Offensive
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
3
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Count-root"
>
3
</span>
</span>
</div>
<button
aria-controls="uuid-1"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -1764,39 +2084,71 @@ exports[`reported queue renders reported queue with comments and load more 1`] =
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorPrimary Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
Pre-Mod
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
1
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
<span
className="Marker-root Marker-colorPrimary Marker-variantRegular"
>
Pre-Mod
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Count-root"
>
1
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
1
</span>
</span>
</div>
<button
aria-controls="uuid-2"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
1
</span>
</span>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -2172,21 +2524,53 @@ exports[`single comment view accepts single comment 1`] = `
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Spam
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
2
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -2308,21 +2692,53 @@ exports[`single comment view rejects single comment 1`] = `
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Spam
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
2
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -2479,21 +2895,53 @@ exports[`single comment view renders single comment view 1`] = `
</a>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
className="HorizontalGutter-root HorizontalGutter-double"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
<div
className="Flex-root Flex-flex"
>
<span>
Spam
</span>
<span
className="Count-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
2
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
@@ -400,7 +400,7 @@ describe("reported queue", () => {
hasNextPage: false,
},
};
}) as any,
}),
},
}),
},
@@ -410,6 +410,52 @@ describe("reported queue", () => {
await waitForElement(() => getByTestID("moderate-container"));
expect(toJSON(getByTestID("moderate-main-container"))).toMatchSnapshot();
});
it("show details of comment with flags", async () => {
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: () =>
pureMerge(emptyModerationQueues, {
reported: {
count: 1,
comments: createQueryResolverStub<
ModerationQueueToCommentsResolver
>(({ variables }) => {
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[0].createdAt,
hasNextPage: false,
},
};
}),
},
}),
},
}),
});
const { getByTestID } = within(testRenderer.root);
const reported = await waitForElement(() =>
getByTestID(`moderate-comment-${reportedComments[0].id}`)
);
expect(
within(reported).queryByText(
reportedComments[0].flags.nodes[0].additionalDetails!
)
).toBeNull();
within(reported)
.getByText("Details", { selector: "button" })
.props.onClick();
within(reported).getByText(
reportedComments[0].flags.nodes[0].additionalDetails!
);
});
it("shows a moderate story", async () => {
const {
testRenderer,
+1
View File
@@ -1,3 +1,4 @@
export { default as useEffectAfterMount } from "./useEffectAfterMount";
export { default as usePrevious } from "./usePrevious";
export { default as useEffectWhenChanged } from "./useEffectWhenChanged";
export { default as useUUID } from "./useUUID";
@@ -0,0 +1,11 @@
import { useMemo } from "react";
import { useTalkContext } from "talk-framework/lib/bootstrap";
/**
* useUUID returns a unique identifier.
*/
export default function useUUID(): string {
const { uuidGenerator } = useTalkContext();
return useMemo(() => uuidGenerator(), []);
}
+22
View File
@@ -9,6 +9,17 @@
url("typeface-source-sans-pro/files/source-sans-pro-latin-300.woff")
format("woff");
}
@font-face {
font-family: "Source Sans Pro";
font-style: italic;
font-display: block;
font-weight: 300;
src: local("Source Sans Pro Light "), local("Source Sans Pro-Light"),
url("typeface-source-sans-pro/files/source-sans-pro-latin-300italic.woff2")
format("woff2"),
url("typeface-source-sans-pro/files/source-sans-pro-latin-300italic.woff")
format("woff");
}
@font-face {
font-family: "Source Sans Pro";
@@ -22,6 +33,17 @@
url("typeface-source-sans-pro/files/source-sans-pro-latin-400.woff")
format("woff");
}
@font-face {
font-family: "Source Sans Pro";
font-style: italic;
font-display: block;
font-weight: 400;
src: local("Source Sans Pro Light "), local("Source Sans Pro-Light"),
url("typeface-source-sans-pro/files/source-sans-pro-latin-400italic.woff2")
format("woff2"),
url("typeface-source-sans-pro/files/source-sans-pro-latin-400italic.woff")
format("woff");
}
@font-face {
font-family: "Source Sans Pro";
@@ -0,0 +1,14 @@
import Context from "talk-server/graph/tenant/context";
import {
CommentActionConnectionInput,
retrieveCommentActionConnection,
} from "talk-server/models/action/comment";
export default (ctx: Context) => ({
connection: ({ first = 10, after, filter }: CommentActionConnectionInput) =>
retrieveCommentActionConnection(ctx.mongo, ctx.tenant.id, {
first,
after,
filter,
}),
});
@@ -1,6 +1,7 @@
import Context from "talk-server/graph/tenant/context";
import Auth from "./Auth";
import CommentActions from "./CommentActions";
import CommentModerationActions from "./CommentModerationActions";
import Comments from "./Comments";
import Stories from "./Stories";
@@ -8,6 +9,7 @@ import Users from "./Users";
export default (ctx: Context) => ({
Auth: Auth(ctx),
CommentActions: CommentActions(ctx),
CommentModerationActions: CommentModerationActions(ctx),
Stories: Stories(ctx),
Comments: Comments(ctx),
@@ -5,7 +5,10 @@ import {
GQLComment,
GQLCommentTypeResolver,
} from "talk-server/graph/tenant/schema/__generated__/types";
import { decodeActionCounts } from "talk-server/models/action/comment";
import {
ACTION_TYPE,
decodeActionCounts,
} from "talk-server/models/action/comment";
import * as comment from "talk-server/models/comment";
import { getLatestRevision } from "talk-server/models/comment";
import { createConnection } from "talk-server/models/helpers/connection";
@@ -64,6 +67,15 @@ export const Comment: GQLCommentTypeResolver<comment.Comment> = {
: createConnection(),
// Action Counts are encoded, decode them for use with the GraphQL system.
actionCounts: c => decodeActionCounts(c.actionCounts),
flags: ({ id }, { first = 10, after }, ctx) =>
ctx.loaders.CommentActions.connection({
first,
after,
filter: {
actionType: ACTION_TYPE.FLAG,
commentID: id,
},
}),
viewerActionPresence: (c, input, ctx) =>
ctx.user ? ctx.loaders.Comments.retrieveMyActionPresence.load(c.id) : null,
parentCount: c => (c.parentID ? c.grandparentIDs.length + 1 : 0),
@@ -0,0 +1,12 @@
import { GQLFlagTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import * as actions from "talk-server/models/action/comment";
export const Flag: GQLFlagTypeResolver<actions.CommentAction> = {
flagger: ({ userID }, args, ctx) => {
if (userID) {
return ctx.loaders.Users.user.load(userID);
}
return null;
},
};
@@ -13,6 +13,7 @@ import { CommentModerationAction } from "./CommentModerationAction";
import { CommentRevision } from "./CommentRevision";
import { DisableCommenting } from "./DisableCommenting";
import { FacebookAuthIntegration } from "./FacebookAuthIntegration";
import { Flag } from "./Flag";
import { GoogleAuthIntegration } from "./GoogleAuthIntegration";
import { ModerationQueue } from "./ModerationQueue";
import { ModerationQueues } from "./ModerationQueues";
@@ -42,6 +43,7 @@ const Resolvers: GQLResolver = {
Cursor,
DisableCommenting,
FacebookAuthIntegration,
Flag,
GoogleAuthIntegration,
ModerationQueue,
ModerationQueues,
@@ -182,6 +182,54 @@ type FlagReasonActionCounts {
COMMENT_DETECTED_SUSPECT_WORD: Int!
}
type Flag {
"""
flagger is the User that created the Flag. If this is null, then the system
created the Flag.
"""
flagger: User
"""
reason is the selected reason why the Flag is being created.
"""
reason: COMMENT_FLAG_REASON!
"""
additionalDetails stores information from the User as to why the Flag was
created or is relevant.
"""
additionalDetails: String
}
type FlagEdge {
"""
node is the Flag for this edge.
"""
node: Flag!
"""
cursor is used in pagination.
"""
cursor: Cursor!
}
type FlagsConnection {
"""
edges are a subset of FlagEdge's.
"""
edges: [FlagEdge!]!
"""
nodes is a list of Flags.
"""
nodes: [Flag!]!
"""
pageInfo is information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
FlagActionCounts stores all the counts for the counts for the flag action on a
given item and the reason counts.
@@ -1690,6 +1738,12 @@ type Comment {
"""
actionCounts: ActionCounts!
"""
flags is the actual Flags that were left by the Users or the system.
"""
flags(first: Int = 10, after: Cursor): FlagsConnection!
@auth(roles: [ADMIN, MODERATOR])
"""
viewerActionPresence stores the presence information for all the actions
left by the current User on this Comment.
+83 -7
View File
@@ -5,14 +5,24 @@ import uuid from "uuid";
import { Omit, Sub } from "talk-common/types";
import {
GQLActionCounts,
GQLActionPresence,
GQLCOMMENT_FLAG_DETECTED_REASON,
GQLCOMMENT_FLAG_REASON,
GQLCOMMENT_FLAG_REPORTED_REASON,
GQLDontAgreeActionCounts,
GQLFlagActionCounts,
GQLReactionActionCounts,
} from "talk-server/graph/tenant/schema/__generated__/types";
import { createIndexFactory } from "talk-server/models/helpers/indexing";
import { FilterQuery } from "talk-server/models/helpers/query";
import {
Connection,
ConnectionInput,
resolveConnection,
} from "talk-server/models/helpers/connection";
import {
createConnectionOrderVariants,
createIndexFactory,
} from "talk-server/models/helpers/indexing";
import Query, { FilterQuery } from "talk-server/models/helpers/query";
import { TenantResource } from "talk-server/models/tenant";
function collection(mongo: Db) {
@@ -40,6 +50,27 @@ export enum ACTION_TYPE {
export type EncodedCommentActionCounts = Record<string, number>;
export type FlagActionCounts = GQLFlagActionCounts;
export interface ActionCounts {
/**
* reaction returns the counts for the reaction action on an item.
*/
reaction: GQLReactionActionCounts;
/**
* dontAgree returns the counts for the dontAgree action on an item. This edge is
* restricted to administrators and moderators.
*/
dontAgree: GQLDontAgreeActionCounts;
/**
* flag returns the counts for the flag action on an item. This edge is
* restricted to administrators and moderators.
*/
flag: FlagActionCounts;
}
/**
* FLAG_REASON is the reason that a given Flag has been created.
*/
@@ -115,6 +146,19 @@ export async function createCommentActionIndexes(mongo: Db) {
{ tenantID: 1, actionType: 1, commentID: 1, userID: 1 },
{ background: true }
);
const variants = createConnectionOrderVariants<Readonly<CommentAction>>(
[{ createdAt: -1 }],
{ background: true }
);
// Connection pagination.
// { ...connectionParams }
await variants(createIndex, {
tenantID: 1,
actionType: 1,
commentID: 1,
});
}
const ActionSchema = [
@@ -254,6 +298,38 @@ export async function createActions(
);
}
export type CommentActionConnectionInput = ConnectionInput<CommentAction>;
export async function retrieveCommentActionConnection(
mongo: Db,
tenantID: string,
input: CommentActionConnectionInput
): Promise<Readonly<Connection<Readonly<CommentAction>>>> {
// Create the query.
const query = new Query(collection(mongo)).where({ tenantID });
// If a filter is being applied, filter it as well.
if (input.filter) {
query.where(input.filter);
}
return retrieveConnection(input, query);
}
async function retrieveConnection(
input: CommentActionConnectionInput,
query: Query<CommentAction>
): Promise<Readonly<Connection<Readonly<CommentAction>>>> {
// Apply the pagination arguments to the query.
query.orderBy({ createdAt: -1 });
if (input.after) {
query.where({ createdAt: { $lt: input.after as Date } });
}
// Return a connection.
return resolveConnection(query, input, action => action.createdAt);
}
export async function retrieveUserAction(
mongo: Db,
tenantID: string,
@@ -502,7 +578,7 @@ function decodeActionCountKey(key: string): DecodedActionCountKey {
/**
* createEmptyActionCounts creates a default/empty set of action counts.
*/
function createEmptyActionCounts(): GQLActionCounts {
function createEmptyActionCounts(): ActionCounts {
return {
reaction: {
total: 0,
@@ -557,9 +633,9 @@ export function countTotalActionCounts(
*/
export function decodeActionCounts(
encodedActionCounts: EncodedCommentActionCounts
): GQLActionCounts {
): ActionCounts {
// Default all the action counts to zero.
const actionCounts: GQLActionCounts = createEmptyActionCounts();
const actionCounts: ActionCounts = createEmptyActionCounts();
// Loop over all the encoded action counts to extract each of the action
// counts as they are encoded.
@@ -575,7 +651,7 @@ export function decodeActionCounts(
}
function incrementActionCounts(
actionCounts: GQLActionCounts,
actionCounts: ActionCounts,
actionType: ACTION_TYPE,
reason: GQLCOMMENT_FLAG_REASON | undefined,
count: number = 1
+4
View File
@@ -317,6 +317,10 @@ moderate-marker-karma = Karma
moderate-marker-bodyCount = Body Count
moderate-marker-offensive = Offensive
moderate-markers-details = Details
moderate-flagDetails-offensive = Offensive
moderate-flagDetails-spam = Spam
moderate-emptyQueue-pending = Nicely done! There are no more pending comments to moderate.
moderate-emptyQueue-reported = Nicely done! There are no more reported comments to moderate.
moderate-emptyQueue-unmoderated = Nicely done! All comments have been moderated.