mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 02:43:46 +08:00
feat: added toxic labels (#2396)
This commit is contained in:
@@ -6,6 +6,7 @@ import { Field } from "react-final-form";
|
||||
import { formatPercentage, parsePercentage } from "coral-framework/lib/form";
|
||||
|
||||
import {
|
||||
TOXICITY_ENDPOINT_DEFAULT,
|
||||
TOXICITY_MODEL_DEFAULT,
|
||||
TOXICITY_THRESHOLD_DEFAULT,
|
||||
} from "coral-common/constants";
|
||||
@@ -220,7 +221,7 @@ const PerspectiveConfig: FunctionComponent<Props> = ({ disabled }) => {
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-moderation-perspective-defaultEndpoint"
|
||||
$default="https://commentanalyzer.googleapis.com/v1alpha1"
|
||||
$default={TOXICITY_ENDPOINT_DEFAULT}
|
||||
>
|
||||
<InputDescription>
|
||||
By default the endpoint is set to $default. You may override this
|
||||
@@ -242,6 +243,7 @@ const PerspectiveConfig: FunctionComponent<Props> = ({ disabled }) => {
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder={TOXICITY_ENDPOINT_DEFAULT}
|
||||
spellCheck={false}
|
||||
/>
|
||||
{meta.touched && (meta.error || meta.submitError) && (
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.category {
|
||||
color: var(--palette-error-darkest);
|
||||
}
|
||||
@@ -2,8 +2,6 @@ import React, { FunctionComponent } from "react";
|
||||
|
||||
import { HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
import styles from "./FlagDetailsCategory.css";
|
||||
|
||||
interface Props {
|
||||
category: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
@@ -15,9 +13,7 @@ const FlagDetailsCategory: FunctionComponent<Props> = ({
|
||||
}) => {
|
||||
return (
|
||||
<HorizontalGutter size="half">
|
||||
<Typography variant="bodyCopyBold" className={styles.category}>
|
||||
{category}
|
||||
</Typography>
|
||||
<Typography variant="bodyCopyBold">{category}</Typography>
|
||||
<HorizontalGutter size="half">{children}</HorizontalGutter>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
|
||||
@@ -1,80 +1,94 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { FlagDetailsContainer_comment as CommentData } from "coral-admin/__generated__/FlagDetailsContainer_comment.graphql";
|
||||
import { FlagDetailsContainer_comment } from "coral-admin/__generated__/FlagDetailsContainer_comment.graphql";
|
||||
import { FlagDetailsContainer_settings } from "coral-admin/__generated__/FlagDetailsContainer_settings.graphql";
|
||||
import NotAvailable from "coral-admin/components/NotAvailable";
|
||||
import { TOXICITY_THRESHOLD_DEFAULT } from "coral-common/constants";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { GQLCOMMENT_FLAG_REASON } from "coral-framework/schema";
|
||||
import { HorizontalGutter } from "coral-ui/components";
|
||||
|
||||
import FlagDetailsCategory from "./FlagDetailsCategory";
|
||||
import FlagDetailsEntry from "./FlagDetailsEntry";
|
||||
import ToxicityLabel from "./ToxicityLabel";
|
||||
|
||||
interface Props {
|
||||
comment: CommentData;
|
||||
comment: FlagDetailsContainer_comment;
|
||||
settings: FlagDetailsContainer_settings;
|
||||
}
|
||||
|
||||
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, i) => {
|
||||
switch (n.reason) {
|
||||
case GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_OFFENSIVE:
|
||||
offensiveList.push(
|
||||
const FlagDetailsContainer: FunctionComponent<Props> = ({
|
||||
comment: {
|
||||
revision: { metadata },
|
||||
flags: { nodes },
|
||||
},
|
||||
settings,
|
||||
}) => {
|
||||
const offensive = nodes.filter(
|
||||
({ reason }) => reason === GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_OFFENSIVE
|
||||
);
|
||||
const spam = nodes.filter(
|
||||
({ reason }) => reason === GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_SPAM
|
||||
);
|
||||
|
||||
return (
|
||||
<HorizontalGutter size="oneAndAHalf">
|
||||
{metadata.perspective && (
|
||||
<FlagDetailsCategory
|
||||
category={
|
||||
<Localized id="moderate-flagDetails-toxicityScore">
|
||||
<span>Toxicity Score</span>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<ToxicityLabel
|
||||
score={metadata.perspective.score}
|
||||
threshold={
|
||||
settings.integrations.perspective.threshold ||
|
||||
TOXICITY_THRESHOLD_DEFAULT / 100
|
||||
}
|
||||
/>
|
||||
</FlagDetailsCategory>
|
||||
)}
|
||||
{offensive.length > 0 && (
|
||||
<FlagDetailsCategory
|
||||
category={
|
||||
<Localized id="moderate-flagDetails-offensive">
|
||||
<span>Offensive</span>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
{offensive.map((flag, i) => (
|
||||
<FlagDetailsEntry
|
||||
key={i}
|
||||
user={n.flagger ? n.flagger.username : <NotAvailable />}
|
||||
details={n.additionalDetails}
|
||||
user={flag.flagger ? flag.flagger.username : <NotAvailable />}
|
||||
details={flag.additionalDetails}
|
||||
/>
|
||||
);
|
||||
return;
|
||||
case GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_SPAM:
|
||||
spamList.push(
|
||||
))}
|
||||
</FlagDetailsCategory>
|
||||
)}
|
||||
{spam.length > 0 && (
|
||||
<FlagDetailsCategory
|
||||
category={
|
||||
<Localized id="moderate-flagDetails-spam">
|
||||
<span>Spam</span>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
{spam.map((flag, i) => (
|
||||
<FlagDetailsEntry
|
||||
key={i}
|
||||
user={n.flagger ? n.flagger.username : <NotAvailable />}
|
||||
details={n.additionalDetails}
|
||||
user={flag.flagger ? flag.flagger.username : <NotAvailable />}
|
||||
details={flag.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-spam">
|
||||
<span>Spam</span>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
{spamList}
|
||||
</FlagDetailsCategory>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
);
|
||||
}
|
||||
}
|
||||
))}
|
||||
</FlagDetailsCategory>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
comment: graphql`
|
||||
@@ -88,6 +102,22 @@ const enhanced = withFragmentContainer<Props>({
|
||||
additionalDetails
|
||||
}
|
||||
}
|
||||
revision {
|
||||
metadata {
|
||||
perspective {
|
||||
score
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment FlagDetailsContainer_settings on Settings {
|
||||
integrations {
|
||||
perspective {
|
||||
threshold
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(FlagDetailsContainer);
|
||||
|
||||
@@ -14,3 +14,7 @@
|
||||
.detailsText {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detailsDivider {
|
||||
border: 1px solid var(--palette-grey-lightest);
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ const Markers: FunctionComponent<Props> = ({ children, details }) => {
|
||||
showDetails,
|
||||
]);
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<Flex>
|
||||
<Flex itemGutter>{children}</Flex>
|
||||
<HorizontalGutter>
|
||||
<Flex itemGutter>
|
||||
{children}
|
||||
{details && (
|
||||
<Button
|
||||
size="small"
|
||||
@@ -39,7 +39,12 @@ const Markers: FunctionComponent<Props> = ({ children, details }) => {
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
{showDetails && <div id={uuid}>{details}</div>}
|
||||
{showDetails && (
|
||||
<div id={uuid}>
|
||||
<hr className={styles.detailsDivider} />
|
||||
{details}
|
||||
</div>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import { TOXICITY_THRESHOLD_DEFAULT } from "coral-common/constants";
|
||||
import { removeFragmentRefs } from "coral-framework/testHelpers";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
|
||||
@@ -12,19 +13,33 @@ it("renders all markers", () => {
|
||||
const props: PropTypesOf<typeof MarkersContainerN> = {
|
||||
comment: {
|
||||
status: "PREMOD",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_DETECTED_TOXIC: 1,
|
||||
COMMENT_DETECTED_SPAM: 1,
|
||||
COMMENT_DETECTED_TRUST: 1,
|
||||
COMMENT_DETECTED_LINKS: 1,
|
||||
COMMENT_DETECTED_BANNED_WORD: 1,
|
||||
COMMENT_DETECTED_SUSPECT_WORD: 1,
|
||||
COMMENT_REPORTED_OFFENSIVE: 2,
|
||||
COMMENT_REPORTED_SPAM: 3,
|
||||
revision: {
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_DETECTED_TOXIC: 1,
|
||||
COMMENT_DETECTED_SPAM: 1,
|
||||
COMMENT_DETECTED_TRUST: 1,
|
||||
COMMENT_DETECTED_LINKS: 1,
|
||||
COMMENT_DETECTED_BANNED_WORD: 1,
|
||||
COMMENT_DETECTED_SUSPECT_WORD: 1,
|
||||
COMMENT_REPORTED_OFFENSIVE: 2,
|
||||
COMMENT_REPORTED_SPAM: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
perspective: {
|
||||
score: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
integrations: {
|
||||
perspective: {
|
||||
threshold: TOXICITY_THRESHOLD_DEFAULT / 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -37,19 +52,33 @@ it("renders some markers", () => {
|
||||
const props: PropTypesOf<typeof MarkersContainerN> = {
|
||||
comment: {
|
||||
status: "PREMOD",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_DETECTED_TOXIC: 1,
|
||||
COMMENT_DETECTED_SPAM: 0,
|
||||
COMMENT_DETECTED_TRUST: 1,
|
||||
COMMENT_DETECTED_LINKS: 0,
|
||||
COMMENT_DETECTED_BANNED_WORD: 1,
|
||||
COMMENT_DETECTED_SUSPECT_WORD: 0,
|
||||
COMMENT_REPORTED_OFFENSIVE: 2,
|
||||
COMMENT_REPORTED_SPAM: 0,
|
||||
revision: {
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_DETECTED_TOXIC: 1,
|
||||
COMMENT_DETECTED_SPAM: 0,
|
||||
COMMENT_DETECTED_TRUST: 1,
|
||||
COMMENT_DETECTED_LINKS: 0,
|
||||
COMMENT_DETECTED_BANNED_WORD: 1,
|
||||
COMMENT_DETECTED_SUSPECT_WORD: 0,
|
||||
COMMENT_REPORTED_OFFENSIVE: 2,
|
||||
COMMENT_REPORTED_SPAM: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
perspective: {
|
||||
score: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
integrations: {
|
||||
perspective: {
|
||||
threshold: TOXICITY_THRESHOLD_DEFAULT / 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,26 +2,30 @@ import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { MarkersContainer_comment as CommentData } from "coral-admin/__generated__/MarkersContainer_comment.graphql";
|
||||
import { MarkersContainer_comment } from "coral-admin/__generated__/MarkersContainer_comment.graphql";
|
||||
import { MarkersContainer_settings } from "coral-admin/__generated__/MarkersContainer_settings.graphql";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { Marker, MarkerCount } from "coral-ui/components";
|
||||
import FlagDetailsContainer from "./FlagDetailsContainer";
|
||||
import Markers from "./Markers";
|
||||
|
||||
interface MarkersContainerProps {
|
||||
comment: CommentData;
|
||||
comment: MarkersContainer_comment;
|
||||
settings: MarkersContainer_settings;
|
||||
}
|
||||
|
||||
function hasDetails(c: CommentData) {
|
||||
function hasDetails(c: MarkersContainer_comment) {
|
||||
return (
|
||||
c.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE +
|
||||
c.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM >
|
||||
0
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE +
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM >
|
||||
0 || c.revision.metadata.perspective
|
||||
);
|
||||
}
|
||||
|
||||
let keyCounter = 0;
|
||||
const markers: Array<(c: CommentData) => React.ReactElement<any> | null> = [
|
||||
const markers: Array<
|
||||
(c: MarkersContainer_comment) => React.ReactElement<any> | null
|
||||
> = [
|
||||
c =>
|
||||
(c.status === "PREMOD" && (
|
||||
<Localized id="moderate-marker-preMod" key={keyCounter++}>
|
||||
@@ -30,21 +34,21 @@ const markers: Array<(c: CommentData) => React.ReactElement<any> | null> = [
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.actionCounts.flag.reasons.COMMENT_DETECTED_LINKS && (
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_LINKS && (
|
||||
<Localized id="moderate-marker-link" key={keyCounter++}>
|
||||
<Marker color="primary">Link</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.actionCounts.flag.reasons.COMMENT_DETECTED_BANNED_WORD && (
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_BANNED_WORD && (
|
||||
<Localized id="moderate-marker-bannedWord" key={keyCounter++}>
|
||||
<Marker color="error">Banned Word</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.actionCounts.flag.reasons.COMMENT_DETECTED_SUSPECT_WORD && (
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SUSPECT_WORD && (
|
||||
<Localized id="moderate-marker-suspectWord" key={keyCounter++}>
|
||||
<Marker color="error" variant="filled">
|
||||
Suspect Word
|
||||
@@ -53,46 +57,46 @@ const markers: Array<(c: CommentData) => React.ReactElement<any> | null> = [
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.actionCounts.flag.reasons.COMMENT_DETECTED_SPAM && (
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SPAM && (
|
||||
<Localized id="moderate-marker-spam" key={keyCounter++}>
|
||||
<Marker color="error">Spam</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.actionCounts.flag.reasons.COMMENT_DETECTED_TOXIC && (
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_TOXIC && (
|
||||
<Localized id="moderate-marker-toxic" key={keyCounter++}>
|
||||
<Marker color="error">Toxic</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.actionCounts.flag.reasons.COMMENT_DETECTED_TRUST && (
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_TRUST && (
|
||||
<Localized id="moderate-marker-karma" key={keyCounter++}>
|
||||
<Marker color="error">Karma</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE && (
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE && (
|
||||
<Marker key={keyCounter++} color="error">
|
||||
<Localized id="moderate-marker-offensive">
|
||||
<span>Offensive</span>
|
||||
</Localized>{" "}
|
||||
<MarkerCount>
|
||||
{c.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE}
|
||||
{c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE}
|
||||
</MarkerCount>
|
||||
</Marker>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM && (
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM && (
|
||||
<Marker key={keyCounter++} color="error">
|
||||
<Localized id="moderate-marker-spam">
|
||||
<span>Spam</span>
|
||||
</Localized>{" "}
|
||||
<MarkerCount>
|
||||
{c.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM}
|
||||
{c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM}
|
||||
</MarkerCount>
|
||||
</Marker>
|
||||
)) ||
|
||||
@@ -102,20 +106,25 @@ const markers: Array<(c: CommentData) => React.ReactElement<any> | null> = [
|
||||
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 (
|
||||
<Markers
|
||||
details={
|
||||
hasDetails(this.props.comment) ? (
|
||||
<FlagDetailsContainer comment={this.props.comment} />
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{elements}
|
||||
</Markers>
|
||||
);
|
||||
const doesHaveDetails = hasDetails(this.props.comment);
|
||||
if (elements.length === 0 && !doesHaveDetails) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
|
||||
return (
|
||||
<Markers
|
||||
details={
|
||||
doesHaveDetails ? (
|
||||
<FlagDetailsContainer
|
||||
comment={this.props.comment}
|
||||
settings={this.props.settings}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{elements}
|
||||
</Markers>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,22 +133,34 @@ const enhanced = withFragmentContainer<MarkersContainerProps>({
|
||||
fragment MarkersContainer_comment on Comment {
|
||||
...FlagDetailsContainer_comment
|
||||
status
|
||||
actionCounts {
|
||||
flag {
|
||||
reasons {
|
||||
COMMENT_DETECTED_TOXIC
|
||||
COMMENT_DETECTED_SPAM
|
||||
COMMENT_DETECTED_TRUST
|
||||
COMMENT_DETECTED_LINKS
|
||||
COMMENT_DETECTED_BANNED_WORD
|
||||
COMMENT_DETECTED_SUSPECT_WORD
|
||||
COMMENT_REPORTED_OFFENSIVE
|
||||
COMMENT_REPORTED_SPAM
|
||||
revision {
|
||||
actionCounts {
|
||||
flag {
|
||||
reasons {
|
||||
COMMENT_DETECTED_TOXIC
|
||||
COMMENT_DETECTED_SPAM
|
||||
COMMENT_DETECTED_TRUST
|
||||
COMMENT_DETECTED_LINKS
|
||||
COMMENT_DETECTED_BANNED_WORD
|
||||
COMMENT_DETECTED_SUSPECT_WORD
|
||||
COMMENT_REPORTED_OFFENSIVE
|
||||
COMMENT_REPORTED_SPAM
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata {
|
||||
perspective {
|
||||
score
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment MarkersContainer_settings on Settings {
|
||||
...FlagDetailsContainer_settings
|
||||
}
|
||||
`,
|
||||
})(MarkersContainer);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -16,6 +16,7 @@ const baseProps: PropTypesOf<typeof ModerateCardN> = {
|
||||
body: "content",
|
||||
inReplyTo: null,
|
||||
comment: {},
|
||||
settings: {},
|
||||
status: "undecided",
|
||||
featured: false,
|
||||
viewContextHref: "http://localhost/comment",
|
||||
|
||||
@@ -29,6 +29,7 @@ interface Props {
|
||||
body: string;
|
||||
inReplyTo: string | null;
|
||||
comment: PropTypesOf<typeof MarkersContainer>["comment"];
|
||||
settings: PropTypesOf<typeof MarkersContainer>["settings"];
|
||||
status: "approved" | "rejected" | "undecided";
|
||||
featured: boolean;
|
||||
moderatedBy: React.ReactNode | null;
|
||||
@@ -60,6 +61,7 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
body,
|
||||
inReplyTo,
|
||||
comment,
|
||||
settings,
|
||||
viewContextHref,
|
||||
status,
|
||||
featured,
|
||||
@@ -146,7 +148,7 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
<MarkersContainer comment={comment} />
|
||||
<MarkersContainer comment={comment} settings={settings} />
|
||||
</HorizontalGutter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,6 +141,7 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
body={comment.body!}
|
||||
inReplyTo={comment.parent && comment.parent.author!.username!}
|
||||
comment={comment}
|
||||
settings={settings}
|
||||
dangling={danglingLogic(comment.status)}
|
||||
status={getStatus(comment)}
|
||||
featured={isFeatured(comment)}
|
||||
@@ -211,6 +212,7 @@ const enhanced = withFragmentContainer<Props>({
|
||||
banned
|
||||
suspect
|
||||
}
|
||||
...MarkersContainer_settings
|
||||
}
|
||||
`,
|
||||
viewer: graphql`
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.root {
|
||||
font-size: calc(14rem / var(--rem-base));
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Counter, Typography } from "coral-ui/components";
|
||||
|
||||
import styles from "./ToxicityLabel.css";
|
||||
|
||||
const ToxicityCounter: FunctionComponent<{ score: number }> = ({ score }) => (
|
||||
<Counter>{Math.round(score * 100)}%</Counter>
|
||||
);
|
||||
|
||||
const ToxicityLabel: FunctionComponent<{
|
||||
score: number;
|
||||
threshold: number;
|
||||
}> = ({ score, threshold }) => {
|
||||
const counter = <ToxicityCounter score={score} />;
|
||||
|
||||
if (score > threshold) {
|
||||
return (
|
||||
<Localized id="moderate-toxicityLabel-likely" score={counter}>
|
||||
<Typography
|
||||
className={styles.root}
|
||||
variant="bodyCopy"
|
||||
color="errorDark"
|
||||
>
|
||||
Likely
|
||||
</Typography>
|
||||
</Localized>
|
||||
);
|
||||
} else if (score <= 0.5) {
|
||||
return (
|
||||
<Localized id="moderate-toxicityLabel-unlikely" score={counter}>
|
||||
<Typography className={styles.root}>Unlikely</Typography>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Localized id="moderate-toxicityLabel-maybe" score={counter}>
|
||||
<Typography className={styles.root}>Maybe</Typography>
|
||||
</Localized>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToxicityLabel;
|
||||
+54
-22
@@ -6,23 +6,39 @@ exports[`renders all markers 1`] = `
|
||||
<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,
|
||||
"revision": 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
"metadata": Object {
|
||||
"perspective": Object {
|
||||
"score": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": "PREMOD",
|
||||
}
|
||||
}
|
||||
settings={
|
||||
Object {
|
||||
"integrations": Object {
|
||||
"perspective": Object {
|
||||
"threshold": 0.8,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -129,23 +145,39 @@ exports[`renders some markers 1`] = `
|
||||
<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,
|
||||
"revision": 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
"metadata": Object {
|
||||
"perspective": Object {
|
||||
"score": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": "PREMOD",
|
||||
}
|
||||
}
|
||||
settings={
|
||||
Object {
|
||||
"integrations": Object {
|
||||
"perspective": Object {
|
||||
"threshold": 0.8,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
+6
@@ -66,6 +66,7 @@ exports[`renders approved correctly 1`] = `
|
||||
</div>
|
||||
<Relay(MarkersContainer)
|
||||
comment={Object {}}
|
||||
settings={Object {}}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</div>
|
||||
@@ -174,6 +175,7 @@ exports[`renders correctly 1`] = `
|
||||
</div>
|
||||
<Relay(MarkersContainer)
|
||||
comment={Object {}}
|
||||
settings={Object {}}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</div>
|
||||
@@ -281,6 +283,7 @@ exports[`renders dangling correctly 1`] = `
|
||||
</div>
|
||||
<Relay(MarkersContainer)
|
||||
comment={Object {}}
|
||||
settings={Object {}}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</div>
|
||||
@@ -390,6 +393,7 @@ exports[`renders rejected correctly 1`] = `
|
||||
</div>
|
||||
<Relay(MarkersContainer)
|
||||
comment={Object {}}
|
||||
settings={Object {}}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</div>
|
||||
@@ -503,6 +507,7 @@ exports[`renders reply correctly 1`] = `
|
||||
</div>
|
||||
<Relay(MarkersContainer)
|
||||
comment={Object {}}
|
||||
settings={Object {}}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</div>
|
||||
@@ -639,6 +644,7 @@ exports[`renders story info 1`] = `
|
||||
</div>
|
||||
<Relay(MarkersContainer)
|
||||
comment={Object {}}
|
||||
settings={Object {}}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</div>
|
||||
|
||||
@@ -352,7 +352,7 @@ comment is toxic, according to Perspective API. By default the threshold is set
|
||||
placeholder="80"
|
||||
spellCheck={false}
|
||||
type="text"
|
||||
value=""
|
||||
value="80"
|
||||
/>
|
||||
<div
|
||||
className="TextField-adornment"
|
||||
@@ -545,7 +545,7 @@ improve the API over time.
|
||||
id="configure-moderation-perspective-customEndpoint"
|
||||
name="integrations.perspective.endpoint"
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
placeholder="https://commentanalyzer.googleapis.com/v1alpha1"
|
||||
spellCheck={false}
|
||||
type="text"
|
||||
value=""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TOXICITY_THRESHOLD_DEFAULT } from "coral-common/constants";
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import {
|
||||
GQLComment,
|
||||
@@ -66,6 +67,7 @@ export const settings = createFixture<GQLSettings>({
|
||||
},
|
||||
perspective: {
|
||||
enabled: false,
|
||||
threshold: TOXICITY_THRESHOLD_DEFAULT / 100,
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
@@ -432,19 +434,22 @@ export const baseComment = createFixture<GQLComment>({
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
},
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_DETECTED_TOXIC: 0,
|
||||
COMMENT_DETECTED_SPAM: 0,
|
||||
COMMENT_DETECTED_TRUST: 0,
|
||||
COMMENT_DETECTED_LINKS: 0,
|
||||
COMMENT_DETECTED_BANNED_WORD: 0,
|
||||
COMMENT_DETECTED_SUSPECT_WORD: 0,
|
||||
COMMENT_REPORTED_OFFENSIVE: 0,
|
||||
COMMENT_REPORTED_SPAM: 0,
|
||||
revision: {
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_DETECTED_TOXIC: 0,
|
||||
COMMENT_DETECTED_SPAM: 0,
|
||||
COMMENT_DETECTED_TRUST: 0,
|
||||
COMMENT_DETECTED_LINKS: 0,
|
||||
COMMENT_DETECTED_BANNED_WORD: 0,
|
||||
COMMENT_DETECTED_SUSPECT_WORD: 0,
|
||||
COMMENT_REPORTED_OFFENSIVE: 0,
|
||||
COMMENT_REPORTED_SPAM: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {},
|
||||
},
|
||||
flags: {
|
||||
nodes: [],
|
||||
@@ -498,17 +503,22 @@ export const reportedComments = createFixtures<GQLComment>(
|
||||
author: users.commenters[0],
|
||||
revision: {
|
||||
id: "comment-0-revision-0",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_SPAM: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
perspective: {
|
||||
score: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
permalink: "http://localhost/comment/0",
|
||||
body:
|
||||
"This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_SPAM: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
nodes: [
|
||||
{
|
||||
@@ -528,17 +538,22 @@ export const reportedComments = createFixtures<GQLComment>(
|
||||
id: "comment-1",
|
||||
revision: {
|
||||
id: "comment-1-revision-1",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_OFFENSIVE: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
perspective: {
|
||||
score: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
permalink: "http://localhost/comment/1",
|
||||
author: users.commenters[1],
|
||||
body: "Don't fool with me",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_OFFENSIVE: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
nodes: [
|
||||
{
|
||||
@@ -563,19 +578,24 @@ export const reportedComments = createFixtures<GQLComment>(
|
||||
id: "comment-2",
|
||||
revision: {
|
||||
id: "comment-2-revision-2",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_SPAM: 1,
|
||||
COMMENT_REPORTED_OFFENSIVE: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
perspective: {
|
||||
score: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
permalink: "http://localhost/comment/2",
|
||||
status: GQLCOMMENT_STATUS.PREMOD,
|
||||
author: users.commenters[2],
|
||||
body: "I think I deserve better",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_SPAM: 1,
|
||||
COMMENT_REPORTED_OFFENSIVE: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
nodes: [
|
||||
{
|
||||
@@ -595,18 +615,23 @@ export const reportedComments = createFixtures<GQLComment>(
|
||||
id: "comment-3",
|
||||
revision: {
|
||||
id: "comment-3-revision-3",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_SPAM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
perspective: {
|
||||
score: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
permalink: "http://localhost/comment/3",
|
||||
status: GQLCOMMENT_STATUS.PREMOD,
|
||||
author: users.commenters[3],
|
||||
body: "World peace at last",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_SPAM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
nodes: [
|
||||
{
|
||||
|
||||
@@ -120,28 +120,24 @@ exports[`approves comment in reported queue: dangling 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-0"
|
||||
aria-expanded={false}
|
||||
@@ -349,28 +345,24 @@ exports[`rejects comment in reported queue: dangling 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-0"
|
||||
aria-expanded={false}
|
||||
@@ -575,28 +567,24 @@ exports[`renders reported queue with comments 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-0"
|
||||
aria-expanded={false}
|
||||
@@ -788,28 +776,24 @@ exports[`renders reported queue with comments 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Offensive
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
3
|
||||
</span>
|
||||
<span>
|
||||
Offensive
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
3
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-1"
|
||||
aria-expanded={false}
|
||||
@@ -1017,28 +1001,24 @@ exports[`renders reported queue with comments 2`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-0"
|
||||
aria-expanded={false}
|
||||
@@ -1230,28 +1210,24 @@ exports[`renders reported queue with comments 2`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Offensive
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
3
|
||||
</span>
|
||||
<span>
|
||||
Offensive
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
3
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-1"
|
||||
aria-expanded={false}
|
||||
@@ -1449,46 +1425,42 @@ exports[`renders reported queue with comments and load more 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorPrimary Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorPrimary Marker-variantRegular"
|
||||
>
|
||||
Pre-Mod
|
||||
Pre-Mod
|
||||
</span>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Offensive
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
className="Count-root"
|
||||
>
|
||||
<span>
|
||||
Offensive
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
1
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
className="Count-root"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
1
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-2"
|
||||
aria-expanded={false}
|
||||
|
||||
@@ -120,28 +120,24 @@ exports[`approves comment in rejected queue: dangling 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-0"
|
||||
aria-expanded={false}
|
||||
@@ -346,28 +342,24 @@ exports[`renders rejected queue with comments 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-0"
|
||||
aria-expanded={false}
|
||||
@@ -559,28 +551,24 @@ exports[`renders rejected queue with comments 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Offensive
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
3
|
||||
</span>
|
||||
<span>
|
||||
Offensive
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
3
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-1"
|
||||
aria-expanded={false}
|
||||
@@ -778,41 +766,37 @@ exports[`renders rejected queue with comments and load more 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Offensive
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
<span>
|
||||
Offensive
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
className="Count-root"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
1
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-2"
|
||||
aria-expanded={false}
|
||||
|
||||
@@ -86,28 +86,24 @@ exports[`approves single comment 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-0"
|
||||
aria-expanded={false}
|
||||
@@ -281,28 +277,24 @@ exports[`rejects single comment 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-0"
|
||||
aria-expanded={false}
|
||||
@@ -511,28 +503,24 @@ exports[`renders single comment view 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span
|
||||
className="Marker-root Marker-colorError Marker-variantRegular"
|
||||
>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
<span>
|
||||
Spam
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="Count-root"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-controls="uuid-0"
|
||||
aria-expanded={false}
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
background-color: var(--palette-grey-dark);
|
||||
}
|
||||
|
||||
.colorError {
|
||||
background-color: var(--palette-error-darkest);
|
||||
}
|
||||
|
||||
.colorInherit {
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
/**
|
||||
* The color of the component. It supports those theme colors that make sense for this component.
|
||||
*/
|
||||
color?: "inherit" | "dark" | "grey" | "primary";
|
||||
color?: "inherit" | "dark" | "grey" | "primary" | "error";
|
||||
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
@@ -34,6 +34,7 @@ const Counter: FunctionComponent<Props> = ({
|
||||
[classes.colorDark]: color === "dark",
|
||||
[classes.colorGrey]: color === "grey",
|
||||
[classes.colorInherit]: color === "inherit",
|
||||
[classes.colorError]: color === "error",
|
||||
[classes.sizeSmall]: size === "sm",
|
||||
},
|
||||
className
|
||||
|
||||
@@ -5,6 +5,7 @@ exports[`renders correctly 1`] = `
|
||||
classes={
|
||||
Object {
|
||||
"colorDark": "Counter-colorDark",
|
||||
"colorError": "Counter-colorError",
|
||||
"colorGrey": "Counter-colorGrey",
|
||||
"colorInherit": "Counter-colorInherit",
|
||||
"colorPrimary": "Counter-colorPrimary",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
composes: markerText from "coral-ui/shared/typography.css";
|
||||
border-left: 1px solid currentColor;
|
||||
margin-left: calc(0.5 * var(--mini-unit));
|
||||
margin-right: calc(-0.5 * var(--mini-unit));
|
||||
margin-right: calc(-0.25 * var(--mini-unit));
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
padding-left: calc(0.5 * var(--mini-unit));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
composes: markerText from "coral-ui/shared/typography.css";
|
||||
color: var(--palette-error-main);
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 4px;
|
||||
border-radius: 20px;
|
||||
padding: 1px var(--mini-unit);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -122,6 +122,10 @@
|
||||
color: var(--palette-error-main);
|
||||
}
|
||||
|
||||
.colorErrorDark {
|
||||
color: var(--palette-error-dark);
|
||||
}
|
||||
|
||||
.colorWarning {
|
||||
color: var(--palette-warning-main);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ interface Props extends PropTypesOf<typeof Box> {
|
||||
| "textLight"
|
||||
| "warning"
|
||||
| "error"
|
||||
| "errorDark"
|
||||
| "success";
|
||||
/**
|
||||
* The container used for the root node.
|
||||
@@ -118,6 +119,7 @@ const Typography: FunctionComponent<Props> = props => {
|
||||
[classes.colorTextLight]: color === "textLight",
|
||||
[classes.colorPrimary]: color === "primary",
|
||||
[classes.colorError]: color === "error",
|
||||
[classes.colorErrorDark]: color === "errorDark",
|
||||
[classes.colorSuccess]: color === "success",
|
||||
[classes.colorWarning]: color === "warning",
|
||||
[classes.noWrap]: noWrap,
|
||||
|
||||
@@ -27,3 +27,10 @@ export const TOXICITY_THRESHOLD_DEFAULT = 80;
|
||||
* TOXICITY_MODEL_DEFAULT is the default value used for the toxicity model.
|
||||
*/
|
||||
export const TOXICITY_MODEL_DEFAULT = "TOXICITY";
|
||||
|
||||
/**
|
||||
* TOXICITY_ENDPOINT_DEFAULT is the default value used for the toxicity endpoint
|
||||
* for the API.
|
||||
*/
|
||||
export const TOXICITY_ENDPOINT_DEFAULT =
|
||||
"https://commentanalyzer.googleapis.com/v1alpha1";
|
||||
|
||||
@@ -14,5 +14,7 @@ export const CommentRevision: Required<
|
||||
comment: w => w.comment,
|
||||
actionCounts: w => decodeActionCounts(w.revision.actionCounts),
|
||||
body: w => w.revision.body,
|
||||
// Defaults to an empty object if not set on the revision.
|
||||
metadata: w => w.revision.metadata || {},
|
||||
createdAt: w => w.revision.createdAt,
|
||||
};
|
||||
|
||||
@@ -750,12 +750,12 @@ type PerspectiveExternalIntegration {
|
||||
"""
|
||||
The endpoint that Coral should use to communicate with the perspective API.
|
||||
"""
|
||||
endpoint: String
|
||||
endpoint: String @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
The key for the Perspective API integration.
|
||||
"""
|
||||
key: String
|
||||
key: String @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
The threshold that given a specific toxic comment score, the comment will
|
||||
@@ -766,12 +766,12 @@ type PerspectiveExternalIntegration {
|
||||
"""
|
||||
model is the Perspective model to use.
|
||||
"""
|
||||
model: String
|
||||
model: String @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
When True, comments sent will not be stored by the Google Perspective API.
|
||||
"""
|
||||
doNotStore: Boolean
|
||||
doNotStore: Boolean @auth(roles: [ADMIN])
|
||||
}
|
||||
|
||||
type ExternalIntegrations {
|
||||
@@ -1158,7 +1158,7 @@ type Settings {
|
||||
"""
|
||||
integrations contains all the external integrations that can be enabled.
|
||||
"""
|
||||
integrations: ExternalIntegrations! @auth(roles: [ADMIN])
|
||||
integrations: ExternalIntegrations! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
karma is the set of settings related to how user Trust and Karma are
|
||||
@@ -1705,6 +1705,23 @@ type CommentModerationActionConnection {
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type CommentRevisionPerspectiveMetadata {
|
||||
"""
|
||||
score is the value detected from the perspective API. This is returned as the
|
||||
percentage chance it would be considered toxic and can be compared to the
|
||||
defined threshold value.
|
||||
"""
|
||||
score: Float!
|
||||
}
|
||||
|
||||
type CommentRevisionMetadata {
|
||||
"""
|
||||
perspective stores metadata associated with the pipeline analysis of this
|
||||
revision's body.
|
||||
"""
|
||||
perspective: CommentRevisionPerspectiveMetadata
|
||||
}
|
||||
|
||||
type CommentRevision {
|
||||
"""
|
||||
id is the identifier of the CommentRevision.
|
||||
@@ -1729,6 +1746,11 @@ type CommentRevision {
|
||||
"""
|
||||
body: String
|
||||
|
||||
"""
|
||||
metadata stores details on a CommentRevision.
|
||||
"""
|
||||
metadata: CommentRevisionMetadata! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
createdAt is the time that the CommentRevision was created.
|
||||
"""
|
||||
|
||||
@@ -42,6 +42,15 @@ function collection<T = Comment>(mongo: Db) {
|
||||
return mongo.collection<Readonly<T>>("comments");
|
||||
}
|
||||
|
||||
export interface RevisionMetadata {
|
||||
akismet?: boolean;
|
||||
linkCount?: number;
|
||||
perspective?: {
|
||||
score: number;
|
||||
model: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Revision stores a Comment's body for a specific edit. Actions can be tied to
|
||||
* a Revision, as can moderation actions.
|
||||
@@ -62,6 +71,11 @@ export interface Revision {
|
||||
*/
|
||||
actionCounts: EncodedCommentActionCounts;
|
||||
|
||||
/**
|
||||
* metadata stores properties on this revision.
|
||||
*/
|
||||
metadata: RevisionMetadata;
|
||||
|
||||
/**
|
||||
* createdAt is the date that this revision was created at.
|
||||
*/
|
||||
@@ -140,11 +154,6 @@ export interface Comment extends TenantResource {
|
||||
*/
|
||||
childCount: number;
|
||||
|
||||
/**
|
||||
* metadata stores the deep Comment properties.
|
||||
*/
|
||||
metadata?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* createdAt is the date that this Comment was created.
|
||||
*/
|
||||
@@ -240,6 +249,7 @@ export type CreateCommentInput = Omit<
|
||||
| "deletedAt"
|
||||
> &
|
||||
Required<Pick<Revision, "body">> &
|
||||
Pick<Revision, "metadata"> &
|
||||
Partial<Pick<Comment, "actionCounts">>;
|
||||
|
||||
export async function createComment(
|
||||
@@ -249,13 +259,14 @@ export async function createComment(
|
||||
now = new Date()
|
||||
) {
|
||||
// Pull out some useful properties from the input.
|
||||
const { body, actionCounts = {}, ...rest } = input;
|
||||
const { body, actionCounts = {}, metadata, ...rest } = input;
|
||||
|
||||
// Generate the revision.
|
||||
const revision: Revision = {
|
||||
id: uuid.v4(),
|
||||
body,
|
||||
actionCounts,
|
||||
metadata,
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
@@ -311,17 +322,14 @@ export async function pushChildCommentIDOntoParent(
|
||||
return result.value;
|
||||
}
|
||||
|
||||
export type EditCommentInput = Pick<
|
||||
Comment,
|
||||
"id" | "authorID" | "status" | "metadata"
|
||||
> & {
|
||||
export type EditCommentInput = Pick<Comment, "id" | "authorID" | "status"> & {
|
||||
/**
|
||||
* lastEditableCommentCreatedAt is the date that the last comment would have
|
||||
* been editable. It is generally derived from the tenant's
|
||||
* `editCommentWindowLength` property.
|
||||
*/
|
||||
lastEditableCommentCreatedAt: Date;
|
||||
} & Required<Pick<Revision, "body">> &
|
||||
} & Required<Pick<Revision, "body" | "metadata">> &
|
||||
Partial<Pick<Comment, "actionCounts">>;
|
||||
|
||||
// Only comments with the following status's can be edited.
|
||||
@@ -401,17 +409,12 @@ export async function editComment(
|
||||
id: uuid.v4(),
|
||||
body,
|
||||
actionCounts,
|
||||
metadata,
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
const update: Record<string, any> = {
|
||||
$set: {
|
||||
status,
|
||||
// Embed all the metadata properties, this may override the existing
|
||||
// metadata, but we won't replace metadata that has been recalculated.
|
||||
// TODO: (wyattjoh) consider if we want to replace the metadata for edited comments instead of supplementing it
|
||||
...dotize({ metadata }),
|
||||
},
|
||||
$set: { status },
|
||||
$push: {
|
||||
revisions: revision,
|
||||
},
|
||||
|
||||
@@ -248,7 +248,7 @@ export async function create(
|
||||
|
||||
export type EditComment = Omit<
|
||||
EditCommentInput,
|
||||
"status" | "authorID" | "lastEditableCommentCreatedAt"
|
||||
"status" | "authorID" | "lastEditableCommentCreatedAt" | "metadata"
|
||||
>;
|
||||
|
||||
export async function edit(
|
||||
|
||||
@@ -40,15 +40,15 @@ describe("compose", () => {
|
||||
it("merges the metadata", async () => {
|
||||
const status = GQLCOMMENT_STATUS.APPROVED;
|
||||
const enhanced = compose([
|
||||
() => ({ metadata: { first: true } }),
|
||||
() => ({ status, metadata: { second: true } }),
|
||||
() => ({ metadata: { third: true } }),
|
||||
() => ({ metadata: { akismet: true } }),
|
||||
() => ({ metadata: { linkCount: 1 } }),
|
||||
() => ({ status, metadata: { akismet: false } }),
|
||||
]);
|
||||
|
||||
await expect(enhanced(context)).resolves.toEqual({
|
||||
body: context.comment.body,
|
||||
status,
|
||||
metadata: { first: true, second: true },
|
||||
metadata: { akismet: false, linkCount: 1 },
|
||||
actions: [],
|
||||
tags: [],
|
||||
});
|
||||
@@ -104,14 +104,14 @@ describe("compose", () => {
|
||||
|
||||
it("handles when it does not return a status", async () => {
|
||||
const enhanced = compose([
|
||||
() => ({ metadata: { first: true } }),
|
||||
() => ({ metadata: { second: true } }),
|
||||
() => ({ metadata: { akismet: true } }),
|
||||
() => ({ metadata: { akismet: false } }),
|
||||
]);
|
||||
|
||||
await expect(enhanced(context)).resolves.toEqual({
|
||||
body: context.comment.body,
|
||||
status: GQLCOMMENT_STATUS.NONE,
|
||||
metadata: { first: true, second: true },
|
||||
metadata: { akismet: false },
|
||||
actions: [],
|
||||
tags: [],
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Omit, Promiseable, RequireProperty } from "coral-common/types";
|
||||
import { GQLCOMMENT_STATUS } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
import { CreateActionInput } from "coral-server/models/action/comment";
|
||||
import { EditCommentInput } from "coral-server/models/comment";
|
||||
import {
|
||||
EditCommentInput,
|
||||
RevisionMetadata,
|
||||
} from "coral-server/models/comment";
|
||||
import { CommentTag } from "coral-server/models/comment/tag";
|
||||
import { Story } from "coral-server/models/story";
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
@@ -18,7 +21,7 @@ export type ModerationAction = Omit<
|
||||
export interface PhaseResult {
|
||||
actions: ModerationAction[];
|
||||
status: GQLCOMMENT_STATUS;
|
||||
metadata: Record<string, any>;
|
||||
metadata: RevisionMetadata;
|
||||
body: string;
|
||||
tags: CommentTag[];
|
||||
}
|
||||
@@ -33,14 +36,19 @@ export interface ModerationPhaseContext {
|
||||
req?: Request;
|
||||
}
|
||||
|
||||
export type ModerationPhase = (
|
||||
export type RootModerationPhase = (
|
||||
context: ModerationPhaseContext
|
||||
) => Promiseable<PhaseResult>;
|
||||
|
||||
export type IntermediatePhaseResult = Partial<PhaseResult> | void;
|
||||
|
||||
export interface IntermediateModerationPhaseContext
|
||||
extends ModerationPhaseContext {
|
||||
metadata: RevisionMetadata;
|
||||
}
|
||||
|
||||
export type IntermediateModerationPhase = (
|
||||
context: ModerationPhaseContext
|
||||
context: IntermediateModerationPhaseContext
|
||||
) => Promiseable<IntermediatePhaseResult>;
|
||||
|
||||
/**
|
||||
@@ -49,7 +57,7 @@ export type IntermediateModerationPhase = (
|
||||
*/
|
||||
export const compose = (
|
||||
phases: IntermediateModerationPhase[]
|
||||
): ModerationPhase => async context => {
|
||||
): RootModerationPhase => async context => {
|
||||
const final: PhaseResult = {
|
||||
status: GQLCOMMENT_STATUS.NONE,
|
||||
body: context.comment.body,
|
||||
@@ -65,8 +73,8 @@ export const compose = (
|
||||
comment: {
|
||||
...context.comment,
|
||||
body: final.body,
|
||||
metadata: final.metadata,
|
||||
},
|
||||
metadata: final.metadata,
|
||||
});
|
||||
if (result) {
|
||||
// If this result contained actions, then we should push it into the
|
||||
@@ -119,4 +127,6 @@ export const compose = (
|
||||
/**
|
||||
* process the comment and return moderation details.
|
||||
*/
|
||||
export const processForModeration: ModerationPhase = compose(moderationPhases);
|
||||
export const processForModeration: RootModerationPhase = compose(
|
||||
moderationPhases
|
||||
);
|
||||
|
||||
@@ -27,7 +27,6 @@ const testCharCount = (settings: Partial<Settings>, length: number) => {
|
||||
};
|
||||
|
||||
export const commentLength: IntermediateModerationPhase = ({
|
||||
story,
|
||||
tenant,
|
||||
comment,
|
||||
}): IntermediatePhaseResult | void => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
GQLCOMMENT_STATUS,
|
||||
} from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
import { ACTION_TYPE } from "coral-server/models/action/comment";
|
||||
import { Comment } from "coral-server/models/comment";
|
||||
import { RevisionMetadata } from "coral-server/models/comment";
|
||||
import { GlobalModerationSettings } from "coral-server/models/settings";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
@@ -13,21 +13,19 @@ import {
|
||||
|
||||
const testPremodLinksEnable = (
|
||||
settings: DeepPartial<GlobalModerationSettings>,
|
||||
comment: Pick<Comment, "metadata">
|
||||
) =>
|
||||
settings.premodLinksEnable && comment.metadata && comment.metadata.linkCount;
|
||||
metadata: RevisionMetadata
|
||||
) => settings.premodLinksEnable && metadata && metadata.linkCount;
|
||||
|
||||
// This phase checks the comment if it has any links in it if the check is
|
||||
// enabled.
|
||||
export const detectLinks: IntermediateModerationPhase = ({
|
||||
story,
|
||||
tenant,
|
||||
comment,
|
||||
metadata,
|
||||
}): IntermediatePhaseResult | void => {
|
||||
if (
|
||||
comment &&
|
||||
(testPremodLinksEnable(tenant, comment) ||
|
||||
(story.settings && testPremodLinksEnable(story.settings, comment)))
|
||||
testPremodLinksEnable(tenant, metadata) ||
|
||||
(story.settings && testPremodLinksEnable(story.settings, metadata))
|
||||
) {
|
||||
// Add the flag related to Trust to the comment.
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,6 @@ export const preModerate: IntermediateModerationPhase = ({
|
||||
// If the settings say that we're in premod mode, then the comment is in
|
||||
// premod status.
|
||||
|
||||
// TODO: (wyattjoh) pull from the story settings.
|
||||
if (
|
||||
testModerationMode(tenant) ||
|
||||
(story.settings && testModerationMode(story.settings))
|
||||
|
||||
@@ -115,7 +115,7 @@ export const spam: IntermediateModerationPhase = async ({
|
||||
],
|
||||
metadata: {
|
||||
// Store the spam result from Akismet in the Comment metadata.
|
||||
akismet: spam,
|
||||
akismet: isSpam,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ describe("storyClosed", () => {
|
||||
storyClosed({
|
||||
story: { closedAt: new Date() } as ModerationPhaseContext["story"],
|
||||
tenant: {} as ModerationPhaseContext["tenant"],
|
||||
comment: {} as ModerationPhaseContext["comment"],
|
||||
author: {} as ModerationPhaseContext["author"],
|
||||
now: new Date(),
|
||||
})
|
||||
).toThrow();
|
||||
@@ -21,8 +19,6 @@ describe("storyClosed", () => {
|
||||
closeCommenting: { auto: true },
|
||||
} as ModerationPhaseContext["tenant"],
|
||||
now: new Date(),
|
||||
comment: {} as ModerationPhaseContext["comment"],
|
||||
author: {} as ModerationPhaseContext["author"],
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
@@ -35,8 +31,6 @@ describe("storyClosed", () => {
|
||||
},
|
||||
} as ModerationPhaseContext["tenant"],
|
||||
now: new Date(),
|
||||
comment: {} as ModerationPhaseContext["comment"],
|
||||
author: {} as ModerationPhaseContext["author"],
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
@@ -54,8 +48,6 @@ describe("storyClosed", () => {
|
||||
tenant: {
|
||||
closeCommenting: { auto: true },
|
||||
} as ModerationPhaseContext["tenant"],
|
||||
comment: {} as ModerationPhaseContext["comment"],
|
||||
author: {} as ModerationPhaseContext["author"],
|
||||
now,
|
||||
})
|
||||
).toBeUndefined();
|
||||
@@ -66,8 +58,6 @@ describe("storyClosed", () => {
|
||||
tenant: {
|
||||
closeCommenting: { auto: true },
|
||||
} as ModerationPhaseContext["tenant"],
|
||||
comment: {} as ModerationPhaseContext["comment"],
|
||||
author: {} as ModerationPhaseContext["author"],
|
||||
now,
|
||||
})
|
||||
).toBeUndefined();
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { StoryClosedError } from "coral-server/errors";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
IntermediatePhaseResult,
|
||||
ModerationPhaseContext,
|
||||
} from "coral-server/services/comments/pipeline";
|
||||
import { getStoryClosedAt } from "coral-server/services/stories";
|
||||
|
||||
// This phase checks to see if the story being processed is closed or not.
|
||||
export const storyClosed: IntermediateModerationPhase = ({
|
||||
export const storyClosed = ({
|
||||
story,
|
||||
tenant,
|
||||
now,
|
||||
}): IntermediatePhaseResult | void => {
|
||||
}: Pick<
|
||||
ModerationPhaseContext,
|
||||
"story" | "tenant" | "now"
|
||||
>): IntermediatePhaseResult | void => {
|
||||
const closedAt = getStoryClosedAt(tenant, story);
|
||||
if (closedAt && closedAt <= now) {
|
||||
throw new StoryClosedError();
|
||||
|
||||
@@ -3,6 +3,7 @@ import ms from "ms";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
import {
|
||||
TOXICITY_ENDPOINT_DEFAULT,
|
||||
TOXICITY_MODEL_DEFAULT,
|
||||
TOXICITY_THRESHOLD_DEFAULT,
|
||||
} from "coral-common/constants";
|
||||
@@ -49,8 +50,7 @@ export const toxic: IntermediateModerationPhase = async ({
|
||||
|
||||
let endpoint = integration.endpoint;
|
||||
if (isNil(endpoint)) {
|
||||
// TODO: (wyattjoh) replace hardcoded default with config.
|
||||
endpoint = "https://commentanalyzer.googleapis.com/v1alpha1";
|
||||
endpoint = TOXICITY_ENDPOINT_DEFAULT;
|
||||
|
||||
log.trace(
|
||||
{ endpoint },
|
||||
@@ -79,7 +79,7 @@ export const toxic: IntermediateModerationPhase = async ({
|
||||
}
|
||||
|
||||
// TODO: (wyattjoh) replace hardcoded default with config.
|
||||
const timeout = ms("500ms");
|
||||
const timeout = ms("800ms");
|
||||
|
||||
try {
|
||||
logger.trace("checking comment toxicity");
|
||||
@@ -125,6 +125,13 @@ export const toxic: IntermediateModerationPhase = async ({
|
||||
}
|
||||
|
||||
log.trace({ score, isToxic, threshold }, "comment was not toxic");
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
// Store the scores from perspective in the Comment metadata.
|
||||
perspective: { model, score },
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
// Rethrow any ToxicCommentError.
|
||||
if (err instanceof ToxicCommentError) {
|
||||
|
||||
@@ -338,6 +338,11 @@ moderate-markers-details = Details
|
||||
moderate-flagDetails-offensive = Offensive
|
||||
moderate-flagDetails-spam = Spam
|
||||
|
||||
moderate-flagDetails-toxicityScore = Toxicity Score
|
||||
moderate-toxicityLabel-likely = Likely <score></score>
|
||||
moderate-toxicityLabel-unlikely = Unlikely <score></score>
|
||||
moderate-toxicityLabel-maybe = Maybe <score></score>
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user