[CORL-218] In Stream Approve and Reject (#2340)

* feat: instream moderation

* fix: tests

* test: add tests for in stream moderation

* fix: lint

* fix: snapshots
This commit is contained in:
Kiwi
2019-06-04 19:27:15 +02:00
committed by GitHub
parent 1cf2f61bfb
commit 593d94ddbb
74 changed files with 1106 additions and 297 deletions
@@ -51,7 +51,7 @@ const FacebookConfig: FunctionComponent<Props> = ({
<HorizontalGutter size="double">
<Localized
id="configure-auth-facebook-toEnableIntegration"
link={<FacebookLink />}
Link={<FacebookLink />}
>
<Typography>
To enable the integration with Facebook Authentication, you need to
@@ -49,7 +49,7 @@ const GoogleConfig: FunctionComponent<Props> = ({ disabled, callbackURL }) => (
<HorizontalGutter size="double">
<Localized
id="configure-auth-google-toEnableIntegration"
link={<GoogleLink />}
Link={<GoogleLink />}
>
<Typography>
To enable the integration with Google Authentication you need to
@@ -70,7 +70,7 @@ const OIDCConfig: FunctionComponent<Props> = ({
>
{disabledInside => (
<HorizontalGutter size="double">
<Localized id="configure-auth-oidc-toLearnMore" link={<OIDCLink />}>
<Localized id="configure-auth-oidc-toLearnMore" Link={<OIDCLink />}>
<Typography>
{"To learn more: https://openid.net/connect/"}
</Typography>
@@ -19,13 +19,13 @@ const InReplyTo: FunctionComponent<Props> = ({ children }) => {
return (
<Flex alignItems="center">
<Icon className={styles.icon}>reply</Icon>{" "}
<Localized id="moderate-comment-inReplyTo" username={<Username />}>
<Localized id="moderate-comment-inReplyTo" Username={<Username />}>
<Typography
variant="timestamp"
container="span"
className={styles.inReplyTo}
>
{"Reply to <username><username>"}
{"Reply to <Username></Username>"}
</Typography>
</Localized>
</Flex>
@@ -91,7 +91,6 @@ const ModerateCard: FunctionComponent<Props> = ({
className={styles.link}
href={viewContextHref}
target="_blank"
external
>
View Context
</TextLink>
@@ -108,7 +107,6 @@ const ModerateCard: FunctionComponent<Props> = ({
<TextLink
className={styles.link}
href={storyHref}
target="_blank"
onClick={onModerateStory}
>
Moderate Story
@@ -11,15 +11,15 @@ exports[`renders correctly 1`] = `
</ForwardRef(forwardRef)>
<Localized
Username={<Username />}
id="moderate-comment-inReplyTo"
username={<Username />}
>
<ForwardRef(forwardRef)
className="InReplyTo-inReplyTo"
container="span"
variant="timestamp"
>
Reply to &lt;username&gt;&lt;username&gt;
Reply to &lt;Username&gt;&lt;/Username&gt;
</ForwardRef(forwardRef)>
</Localized>
</ForwardRef(forwardRef)>
@@ -48,7 +48,6 @@ exports[`renders accepted correctly 1`] = `
>
<withPropsOnChange(TextLinkProps)
className="ModerateCard-link"
external={true}
href="http://localhost/comment"
target="_blank"
>
@@ -146,7 +145,6 @@ exports[`renders correctly 1`] = `
>
<withPropsOnChange(TextLinkProps)
className="ModerateCard-link"
external={true}
href="http://localhost/comment"
target="_blank"
>
@@ -243,7 +241,6 @@ exports[`renders dangling correctly 1`] = `
>
<withPropsOnChange(TextLinkProps)
className="ModerateCard-link"
external={true}
href="http://localhost/comment"
target="_blank"
>
@@ -342,7 +339,6 @@ exports[`renders rejected correctly 1`] = `
>
<withPropsOnChange(TextLinkProps)
className="ModerateCard-link"
external={true}
href="http://localhost/comment"
target="_blank"
>
@@ -445,7 +441,6 @@ exports[`renders reply correctly 1`] = `
>
<withPropsOnChange(TextLinkProps)
className="ModerateCard-link"
external={true}
href="http://localhost/comment"
target="_blank"
>
@@ -542,7 +537,6 @@ exports[`renders story info 1`] = `
>
<withPropsOnChange(TextLinkProps)
className="ModerateCard-link"
external={true}
href="http://localhost/comment"
target="_blank"
>
@@ -574,7 +568,6 @@ exports[`renders story info 1`] = `
className="ModerateCard-link"
href="/story"
onClick={[Function]}
target="_blank"
>
Moderate Story
</withPropsOnChange(TextLinkProps)>
@@ -72,7 +72,7 @@ it("logs out", async () => {
userMenu.props.onClick();
const signOutButton = await waitForElement(() =>
within(testRenderer.root).getByText("Sign Out")
within(testRenderer.root).getByText("Sign Out", { selector: "button" })
);
signOutButton.props.onClick();
@@ -161,7 +161,7 @@ it("change user role", async () => {
TestRenderer.act(() => {
within(popup)
.getByText("Staff")
.getByText("Staff", { selector: "button" })
.props.onClick();
});
@@ -360,7 +360,7 @@ it("ban user", async () => {
TestRenderer.act(() => {
within(popup)
.getByText("Ban User")
.getByText("Ban User", { selector: "button" })
.props.onClick();
});
@@ -434,7 +434,7 @@ it("remove user ban", async () => {
TestRenderer.act(() => {
within(popup)
.getByText("Remove Ban")
.getByText("Remove Ban", { selector: "button" })
.props.onClick();
});
@@ -61,6 +61,12 @@ exports[`change settings: during submit: oidc without errors 1`] = `
target="_blank"
>
https://openid.net/connect/
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
</p>
<hr
@@ -497,6 +503,12 @@ For more information visit:
target="_blank"
>
https://developers.facebook.com/docs/facebook-login/web
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
.
</p>
@@ -772,6 +784,12 @@ exports[`change settings: enable oidc configure box 1`] = `
target="_blank"
>
https://openid.net/connect/
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
</p>
<hr
@@ -1206,6 +1224,12 @@ exports[`change settings: oidc validation errors 1`] = `
target="_blank"
>
https://openid.net/connect/
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
</p>
<hr
@@ -2082,6 +2106,12 @@ integration to register for a new account.
target="_blank"
>
https://openid.net/connect/
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
</p>
<hr
@@ -2736,6 +2766,12 @@ to create and set up a web application. For more information visit:
target="_blank"
>
https://developers.google.com/identity/protocols/OAuth2WebServer#creatingcred
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
.
</p>
@@ -3010,6 +3046,12 @@ For more information visit:
target="_blank"
>
https://developers.facebook.com/docs/facebook-login/web
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
.
</p>
@@ -67,7 +67,7 @@ exports[`rejected queue accepts comment in rejected queue: dangling 1`] = `
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -88,7 +88,6 @@ exports[`rejected queue accepts comment in rejected queue: dangling 1`] = `
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
target="_blank"
>
Moderate Story
</a>
@@ -267,7 +266,7 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -288,7 +287,6 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
target="_blank"
>
Moderate Story
</a>
@@ -454,7 +452,7 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -475,7 +473,6 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
target="_blank"
>
Moderate Story
</a>
@@ -647,7 +644,7 @@ exports[`rejected queue renders rejected queue with comments and load more 1`] =
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -668,7 +665,6 @@ exports[`rejected queue renders rejected queue with comments and load more 1`] =
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
target="_blank"
>
Moderate Story
</a>
@@ -863,7 +859,7 @@ exports[`reported queue accepts comment in reported queue: dangling 1`] = `
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -884,7 +880,6 @@ exports[`reported queue accepts comment in reported queue: dangling 1`] = `
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
target="_blank"
>
Moderate Story
</a>
@@ -1066,7 +1061,7 @@ exports[`reported queue rejects comment in reported queue: dangling 1`] = `
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -1087,7 +1082,6 @@ exports[`reported queue rejects comment in reported queue: dangling 1`] = `
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
target="_blank"
>
Moderate Story
</a>
@@ -1287,7 +1281,7 @@ exports[`reported queue renders reported queue with comments 1`] = `
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -1308,7 +1302,6 @@ exports[`reported queue renders reported queue with comments 1`] = `
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
target="_blank"
>
Moderate Story
</a>
@@ -1474,7 +1467,7 @@ exports[`reported queue renders reported queue with comments 1`] = `
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -1495,7 +1488,6 @@ exports[`reported queue renders reported queue with comments 1`] = `
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
target="_blank"
>
Moderate Story
</a>
@@ -1677,7 +1669,7 @@ exports[`reported queue renders reported queue with comments 2`] = `
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -1698,7 +1690,6 @@ exports[`reported queue renders reported queue with comments 2`] = `
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
target="_blank"
>
Moderate Story
</a>
@@ -1864,7 +1855,7 @@ exports[`reported queue renders reported queue with comments 2`] = `
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -1885,7 +1876,6 @@ exports[`reported queue renders reported queue with comments 2`] = `
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
target="_blank"
>
Moderate Story
</a>
@@ -2057,7 +2047,7 @@ exports[`reported queue renders reported queue with comments and load more 1`] =
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -2078,7 +2068,6 @@ exports[`reported queue renders reported queue with comments and load more 1`] =
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
target="_blank"
>
Moderate Story
</a>
@@ -2519,7 +2508,7 @@ exports[`single comment view accepts single comment 1`] = `
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -2687,7 +2676,7 @@ exports[`single comment view rejects single comment 1`] = `
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -2890,7 +2879,7 @@ exports[`single comment view renders single comment view 1`] = `
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
launch
open_in_new
</span>
</a>
</div>
@@ -191,7 +191,7 @@ it("change story status", async () => {
TestRenderer.act(() => {
within(popup)
.getByText("Closed")
.getByText("Closed", { selector: "button" })
.props.onClick();
});
@@ -205,7 +205,7 @@ it("change story status", async () => {
TestRenderer.act(() => {
within(popup)
.getByText("Open")
.getByText("Open", { selector: "button" })
.props.onClick();
});
@@ -22,13 +22,13 @@ const Username: FunctionComponent<{ username: string }> = ({ username }) => (
<strong>{username}</strong>
);
const ApprovedComment: FunctionComponent<Props> = props => (
const AcceptedComment: FunctionComponent<Props> = props => (
<DecisionItem icon={<AcceptedIcon />}>
<Localized
id="decisionHistory-acceptedCommentBy"
username={<Username username={props.username} />}
Username={<Username username={props.username} />}
>
<Info>{"Accepted comment by <username></username>"}</Info>
<Info>{"Accepted comment by <Username></Username>"}</Info>
</Localized>
<Footer>
<Typography variant="timestamp">
@@ -40,4 +40,4 @@ const ApprovedComment: FunctionComponent<Props> = props => (
</DecisionItem>
);
export default ApprovedComment;
export default AcceptedComment;
@@ -26,9 +26,9 @@ const RejectedComment: FunctionComponent<Props> = props => (
<DecisionItem icon={<RejectedIcon />}>
<Localized
id="decisionHistory-rejectedCommentBy"
username={<Username username={props.username} />}
Username={<Username username={props.username} />}
>
<Info>{"Rejected comment by <username></username>"}</Info>
<Info>{"Rejected comment by <Username></Username>"}</Info>
</Localized>
<Footer>
<Typography variant="timestamp">
@@ -5,15 +5,15 @@ exports[`renders correctly 1`] = `
icon={<AcceptedIcon />}
>
<Localized
id="decisionHistory-acceptedCommentBy"
username={
Username={
<Username
username="InTheExpensiveSeats"
/>
}
id="decisionHistory-acceptedCommentBy"
>
<Info>
Accepted comment by &lt;username&gt;&lt;/username&gt;
Accepted comment by &lt;Username&gt;&lt;/Username&gt;
</Info>
</Localized>
<Footer>
@@ -5,15 +5,15 @@ exports[`renders correctly 1`] = `
icon={<RejectedIcon />}
>
<Localized
id="decisionHistory-rejectedCommentBy"
username={
Username={
<Username
username="InTheExpensiveSeats"
/>
}
id="decisionHistory-rejectedCommentBy"
>
<Info>
Rejected comment by &lt;username&gt;&lt;/username&gt;
Rejected comment by &lt;Username&gt;&lt;/Username&gt;
</Info>
</Localized>
<Footer>
@@ -49,9 +49,9 @@ const SignIn: FunctionComponent<Props> = ({ username, onSignInAs }) => {
</Localized>
</div>
<div>
<Localized id="restricted-signedInAs" username={<Username />}>
<Localized id="restricted-signedInAs" Username={<Username />}>
<Typography variant="bodyCopy" align="center" container="div">
{"You are signed in as: <username></username>"}
{"You are signed in as: <Username></Username>"}
</Typography>
</Localized>
</div>
@@ -40,15 +40,15 @@ exports[`renders correctly 1`] = `
</div>
<div>
<Localized
Username={<Username />}
id="restricted-signedInAs"
username={<Username />}
>
<ForwardRef(forwardRef)
align="center"
container="div"
variant="bodyCopy"
>
You are signed in as: &lt;username&gt;&lt;/username&gt;
You are signed in as: &lt;Username&gt;&lt;/Username&gt;
</ForwardRef(forwardRef)>
</Localized>
</div>
@@ -22,10 +22,10 @@ const UserBoxAuthenticated: FunctionComponent<
<Flex itemGutter="half" wrap>
<Localized
id="general-userBoxAuthenticated-signedInAs"
username={<Username />}
Username={<Username />}
>
<Typography variant="bodyCopy" container="div">
{"Signed in as <username></username>."}
{"Signed in as <Username></Username>."}
</Typography>
</Localized>
{props.showLogoutButton && (
@@ -6,14 +6,14 @@ exports[`renders correctly with logout button 1`] = `
wrap={true}
>
<Localized
Username={<Username />}
id="general-userBoxAuthenticated-signedInAs"
username={<Username />}
>
<ForwardRef(forwardRef)
container="div"
variant="bodyCopy"
>
Signed in as &lt;username&gt;&lt;/username&gt;.
Signed in as &lt;Username&gt;&lt;/Username&gt;.
</ForwardRef(forwardRef)>
</Localized>
<Localized
@@ -48,14 +48,14 @@ exports[`renders correctly without logout button 1`] = `
wrap={true}
>
<Localized
Username={<Username />}
id="general-userBoxAuthenticated-signedInAs"
username={<Username />}
>
<ForwardRef(forwardRef)
container="div"
variant="bodyCopy"
>
Signed in as &lt;username&gt;&lt;/username&gt;.
Signed in as &lt;Username&gt;&lt;/Username&gt;.
</ForwardRef(forwardRef)>
</Localized>
</ForwardRef(forwardRef)>
+15 -6
View File
@@ -1,23 +1,29 @@
enum View {
enum VIEW {
SIGN_UP
SIGN_IN
FORGOT_PASSWORD
}
enum Tab {
enum TAB {
COMMENTS
PROFILE
}
enum ProfileTab {
enum PROFILE_TAB {
MY_COMMENTS
SETTINGS
}
enum COMMENT_VIEWER_ACTION {
EDIT
ACCEPT
REJECT
}
type AuthPopup {
open: Boolean!
focus: Boolean!
view: View
view: VIEW
}
extend type Comment {
@@ -26,6 +32,9 @@ extend type Comment {
# localReplies contains only comments created by the user
# on the ultimate threading level.
localReplies: [Comment!]
# Remember last viewer action that could have caused a status change.
lastViewerAction: COMMENT_VIEWER_ACTION
}
type Local {
@@ -33,13 +42,13 @@ type Local {
accessTokenExp: Int
accessTokenJTI: String
loggedIn: Boolean!
activeTab: Tab!
activeTab: TAB!
authPopup: AuthPopup!
storyID: String
storyURL: String
commentID: String
defaultStreamOrderBy: COMMENT_SORT!
profileTab: ProfileTab!
profileTab: PROFILE_TAB!
bla: [String]
}
@@ -37,6 +37,7 @@ function createDefaultProps(add: DeepPartial<Props> = {}): Props {
},
pending: false,
tags: [],
lastViewerAction: null,
},
settings: {
disableCommenting: {
@@ -3,9 +3,9 @@ import React, { Component, MouseEvent } from "react";
import { graphql } from "react-relay";
import { isBeforeDate } from "coral-common/utils";
import { getURLWithCommentID } from "coral-framework/helpers";
import { getURLWithCommentID, roleIsAtLeast } from "coral-framework/helpers";
import withFragmentContainer from "coral-framework/lib/relay/withFragmentContainer";
import { GQLUSER_STATUS } from "coral-framework/schema";
import { GQLUSER_ROLE, GQLUSER_STATUS } from "coral-framework/schema";
import { PropTypesOf } from "coral-framework/types";
import { CommentContainer_comment as CommentData } from "coral-stream/__generated__/CommentContainer_comment.graphql";
import { CommentContainer_settings as SettingsData } from "coral-stream/__generated__/CommentContainer_settings.graphql";
@@ -23,6 +23,9 @@ import { isCommentVisible } from "../helpers";
import ButtonsBar from "./ButtonsBar";
import EditCommentFormContainer from "./EditCommentForm";
import IndentedComment from "./IndentedComment";
import CaretContainer, {
RejectedTombstoneContainer,
} from "./ModerationDropdown";
import PermalinkButtonContainer from "./PermalinkButton";
import ReactionButtonContainer from "./ReactionButton";
import ReplyButton from "./ReplyButton";
@@ -158,6 +161,9 @@ export class CommentContainer extends Component<Props, State> {
this.props.viewer &&
this.props.viewer.status.current.includes(GQLUSER_STATUS.BANNED)
);
const showCaret =
this.props.viewer &&
roleIsAtLeast(this.props.viewer.role, GQLUSER_ROLE.MODERATOR);
if (showEditDialog) {
return (
<div data-testid={`comment-${comment.id}`}>
@@ -170,10 +176,15 @@ export class CommentContainer extends Component<Props, State> {
</div>
);
}
// Comment is not visible, so don't render it.
// This is the case when the comment was just edited and
// the comment status has changed.
if (!isCommentVisible(comment)) {
// Comment is not visible after viewer rejected it.
if (
comment.lastViewerAction === "REJECT" &&
comment.status === "REJECTED"
) {
return <RejectedTombstoneContainer comment={comment} />;
}
// Comment is not visible after edit, so don't render it anymore.
if (comment.lastViewerAction === "EDIT" && !isCommentVisible(comment)) {
return null;
}
return (
@@ -201,18 +212,20 @@ export class CommentContainer extends Component<Props, State> {
}
tags={comment.tags.map(t => t.name)}
topBarRight={
(editable && (
<Localized id="comments-commentContainer-editButton">
<Button
color="primary"
variant="underlined"
onClick={this.openEditDialog}
>
Edit
</Button>
</Localized>
)) ||
undefined
<Flex itemGutter>
{editable && (
<Localized id="comments-commentContainer-editButton">
<Button
color="primary"
variant="underlined"
onClick={this.openEditDialog}
>
Edit
</Button>
</Localized>
)}
{showCaret && <CaretContainer comment={comment} />}
</Flex>
}
footer={
<>
@@ -292,6 +305,7 @@ const enhanced = withSetCommentIDMutation(
ignoredUsers {
id
}
role
...UsernameWithPopoverContainer_viewer
...ReactionButtonContainer_viewer
...ReportButtonContainer_viewer
@@ -330,10 +344,13 @@ const enhanced = withSetCommentIDMutation(
name
}
pending
lastViewerAction
...ReplyCommentFormContainer_comment
...EditCommentFormContainer_comment
...ReactionButtonContainer_comment
...ReportButtonContainer_comment
...CaretContainer_comment
...RejectedTombstoneContainer_comment
}
`,
settings: graphql`
@@ -51,7 +51,10 @@ function commit(environment: Environment, input: EditCommentInput) {
},
clientMutationId: (clientMutationId++).toString(),
},
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
},
updater: store => {
store.get(input.commentID)!.setValue("EDIT", "lastViewerAction");
},
});
}
@@ -19,13 +19,13 @@ const InReplyTo: FunctionComponent<Props> = ({ username }) => {
return (
<Flex alignItems="center">
<Icon className={styles.icon}>reply</Icon>{" "}
<Localized id="comments-inReplyTo" username={<Username />}>
<Localized id="comments-inReplyTo" Username={<Username />}>
<Typography
variant="timestamp"
container="span"
className={styles.inReplyTo}
>
{"In reply to <username><username>"}
{"In reply to <Username></Username>"}
</Typography>
</Localized>
</Flex>
@@ -0,0 +1,46 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutation,
MutationInput,
} from "coral-framework/lib/relay";
import { AcceptCommentMutation as MutationTypes } from "coral-stream/__generated__/AcceptCommentMutation.graphql";
let clientMutationId = 0;
const AcceptCommentMutation = createMutation(
"acceptComment",
(environment: Environment, input: MutationInput<MutationTypes>) =>
commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation: graphql`
mutation AcceptCommentMutation($input: AcceptCommentInput!) {
acceptComment(input: $input) {
comment {
status
}
clientMutationId
}
}
`,
optimisticResponse: {
acceptComment: {
comment: {
status: "ACCEPTED",
},
},
},
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
updater: store => {
store.get(input.commentID)!.setValue("ACCEPT", "lastViewerAction");
},
})
);
export default AcceptCommentMutation;
@@ -0,0 +1,3 @@
.root {
padding: 2px 4px;
}
@@ -0,0 +1,69 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { graphql } from "react-relay";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { CaretContainer_comment as CommentData } from "coral-stream/__generated__/CaretContainer_comment.graphql";
import { Button, ClickOutside, Icon, Popover } from "coral-ui/components";
import ModerationDropdownContainer from "./ModerationDropdownContainer";
import styles from "./CaretContainer.css";
interface Props {
comment: CommentData;
}
const CaretContainer: FunctionComponent<Props> = props => {
const popoverID = `comments-moderationMenu`;
return (
<Localized
id="comments-moderationDropdown-popover"
attrs={{ description: true }}
>
<Popover
id={popoverID}
placement="bottom-end"
description="A popover menu to moderate the comment"
body={({ toggleVisibility }) => (
<ClickOutside onClickOutside={toggleVisibility}>
<ModerationDropdownContainer
comment={props.comment}
onDismiss={toggleVisibility}
/>
</ClickOutside>
)}
>
{({ toggleVisibility, visible, ref }) => (
<Localized
id="comments-moderationDropdown-caretButton"
attrs={{ "aria-label": true }}
>
<Button
variant="ghost"
size="small"
className={styles.root}
onClick={toggleVisibility}
aria-controls={popoverID}
active={visible}
ref={ref}
aria-label="Moderate"
>
<Icon>{visible ? "expand_less" : "expand_more"}</Icon>
</Button>
</Localized>
)}
</Popover>
</Localized>
);
};
const enhanced = withFragmentContainer<Props>({
comment: graphql`
fragment CaretContainer_comment on Comment {
...ModerationDropdownContainer_comment
}
`,
})(CaretContainer);
export default enhanced;
@@ -0,0 +1,8 @@
.approved {
color: var(--palette-success-dark);
font-weight: var(--font-weight-bold);
}
.rejected {
color: var(--palette-error-dark);
font-weight: var(--font-weight-bold);
}
@@ -0,0 +1,104 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent, useCallback } from "react";
import { graphql } from "react-relay";
import { useMutation, withFragmentContainer } from "coral-framework/lib/relay";
import { ModerationDropdownContainer_comment as CommentData } from "coral-stream/__generated__/ModerationDropdownContainer_comment.graphql";
import {
Dropdown,
DropdownButton,
DropdownDivider,
Icon,
} from "coral-ui/components";
import AcceptCommentMutation from "./AcceptCommentMutation";
import styles from "./ModerationDropdownContainer.css";
import RejectCommentMutation from "./RejectCommentMutation";
interface Props {
comment: CommentData;
onDismiss: () => void;
}
const ModerationDropdownContainer: FunctionComponent<Props> = ({
comment,
onDismiss,
}) => {
const accept = useMutation(AcceptCommentMutation);
const reject = useMutation(RejectCommentMutation);
const onAccept = useCallback(() => {
accept({ commentID: comment.id, commentRevisionID: comment.revision.id });
}, [accept, comment]);
const onReject = useCallback(
() =>
reject({ commentID: comment.id, commentRevisionID: comment.revision.id }),
[accept, comment]
);
const accepted = comment.status === "ACCEPTED";
const rejected = comment.status === "REJECTED";
return (
<Dropdown>
{accepted ? (
<Localized id="comments-moderationDropdown-approved">
<DropdownButton
icon={<Icon className={styles.approved}>check</Icon>}
className={styles.approved}
disabled
>
Approved
</DropdownButton>
</Localized>
) : (
<Localized id="comments-moderationDropdown-approve">
<DropdownButton icon={<Icon>check</Icon>} onClick={onAccept}>
Approve
</DropdownButton>
</Localized>
)}
{rejected ? (
<Localized id="comments-moderationDropdown-rejected">
<DropdownButton
icon={<Icon className={styles.rejected}>close</Icon>}
className={styles.rejected}
disabled
>
Rejected
</DropdownButton>
</Localized>
) : (
<Localized id="comments-moderationDropdown-reject">
<DropdownButton icon={<Icon>close</Icon>} onClick={onReject}>
Reject
</DropdownButton>
</Localized>
)}
<DropdownDivider />
<Localized id="comments-moderationDropdown-goToModerate">
<DropdownButton
href={`/admin/moderate/comment/${comment.id}`}
target="_blank"
anchor
>
Go to Moderate
</DropdownButton>
</Localized>
</Dropdown>
);
};
const enhanced = withFragmentContainer<Props>({
comment: graphql`
fragment ModerationDropdownContainer_comment on Comment {
id
revision {
id
}
status
}
`,
})(ModerationDropdownContainer);
export default enhanced;
@@ -0,0 +1,46 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutation,
MutationInput,
} from "coral-framework/lib/relay";
import { RejectCommentMutation as MutationTypes } from "coral-stream/__generated__/RejectCommentMutation.graphql";
let clientMutationId = 0;
const RejectCommentMutation = createMutation(
"rejectComment",
(environment: Environment, input: MutationInput<MutationTypes>) =>
commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation: graphql`
mutation RejectCommentMutation($input: RejectCommentInput!) {
rejectComment(input: $input) {
comment {
status
}
clientMutationId
}
}
`,
optimisticResponse: {
rejectComment: {
comment: {
status: "REJECTED",
},
},
},
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
updater: store => {
store.get(input.commentID)!.setValue("REJECT", "lastViewerAction");
},
})
);
export default RejectCommentMutation;
@@ -0,0 +1,42 @@
import React, { FunctionComponent, useMemo } from "react";
import { graphql } from "react-relay";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { RejectedTombstoneContainer_comment as CommentData } from "coral-stream/__generated__/RejectedTombstoneContainer_comment.graphql";
import { CallOut, TextLink } from "coral-ui/components";
import { Localized } from "fluent-react/compat";
interface Props {
comment: CommentData;
}
const RejectedTombstoneContainer: FunctionComponent<Props> = ({ comment }) => {
const Link = useMemo<React.FunctionComponent>(
() => ({ children }) => (
<TextLink href={`/admin/moderate/comment/${comment.id}`} target="_blank">
{children}
</TextLink>
),
[comment]
);
return (
<CallOut color="primary" fullWidth>
<Localized id="comments-rejectedTombstone" TextLink={<Link />}>
<div>
You have rejected this comment.{" "}
<Link>Go to Moderate to review this decision.</Link>
</div>
</Localized>
</CallOut>
);
};
const enhanced = withFragmentContainer<Props>({
comment: graphql`
fragment RejectedTombstoneContainer_comment on Comment {
id
}
`,
})(RejectedTombstoneContainer);
export default enhanced;
@@ -0,0 +1,4 @@
export { default, default as CaretContainer } from "./CaretContainer";
export {
default as RejectedTombstoneContainer,
} from "./RejectedTombstoneContainer";
@@ -52,10 +52,10 @@ function commit(environment: Environment, input: CreateCommentReactionInput) {
total: currentCount + 1,
},
},
},
} as any,
clientMutationId: (clientMutationId++).toString(),
},
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
},
});
}
@@ -51,10 +51,10 @@ function commit(environment: Environment, input: RemoveCommentReactionInput) {
total: currentCount - 1,
},
},
},
} as any,
clientMutationId: clientMutationId.toString(),
},
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
},
});
}
@@ -187,7 +187,7 @@ function commit(
},
clientMutationId: (clientMutationId++).toString(),
},
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
},
optimisticUpdater: store => {
// Skip optimistic update if comment is probably premoderated.
if (expectPremoderation) {
@@ -19,9 +19,9 @@ const ReplyTo: FunctionComponent<Props> = ({ username }) => {
return (
<Flex alignItems="center" className={styles.root}>
<Icon>reply</Icon>{" "}
<Localized id="comments-replyTo" username={<Username />}>
<Localized id="comments-replyTo" Username={<Username />}>
<Typography container="span" className={styles.text}>
{"Replying to: <username></username>"}
{"Replying to: <Username></Username>"}
</Typography>
</Localized>
</Flex>
@@ -10,14 +10,14 @@ exports[`renders correctly 1`] = `
</ForwardRef(forwardRef)>
<Localized
Username={<Username />}
id="comments-replyTo"
username={<Username />}
>
<ForwardRef(forwardRef)
className="ReplyTo-text"
container="span"
>
Replying to: &lt;username&gt;&lt;/username&gt;
Replying to: &lt;Username&gt;&lt;/Username&gt;
</ForwardRef(forwardRef)>
</Localized>
</ForwardRef(forwardRef)>
@@ -29,6 +29,7 @@ exports[`hide reply button 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -70,6 +71,7 @@ exports[`hide reply button 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -86,6 +88,11 @@ exports[`hide reply button 1`] = `
parentAuthorName={null}
showEditedMarker={false}
tags={Array []}
topBarRight={
<ForwardRef(forwardRef)
itemGutter={true}
/>
}
username={
<Relay(UsernameWithPopoverContainer)
user={
@@ -131,6 +138,7 @@ exports[`renders body only 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -178,6 +186,7 @@ exports[`renders body only 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -194,6 +203,11 @@ exports[`renders body only 1`] = `
parentAuthorName={null}
showEditedMarker={false}
tags={Array []}
topBarRight={
<ForwardRef(forwardRef)
itemGutter={true}
/>
}
username={
<Relay(UsernameWithPopoverContainer)
user={
@@ -239,6 +253,7 @@ exports[`renders disabled reply when commenting has been disabled 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -286,6 +301,7 @@ exports[`renders disabled reply when commenting has been disabled 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -302,6 +318,11 @@ exports[`renders disabled reply when commenting has been disabled 1`] = `
parentAuthorName={null}
showEditedMarker={false}
tags={Array []}
topBarRight={
<ForwardRef(forwardRef)
itemGutter={true}
/>
}
username={
<Relay(UsernameWithPopoverContainer)
user={
@@ -347,6 +368,7 @@ exports[`renders disabled reply when story is closed 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -394,6 +416,7 @@ exports[`renders disabled reply when story is closed 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -410,6 +433,11 @@ exports[`renders disabled reply when story is closed 1`] = `
parentAuthorName={null}
showEditedMarker={false}
tags={Array []}
topBarRight={
<ForwardRef(forwardRef)
itemGutter={true}
/>
}
username={
<Relay(UsernameWithPopoverContainer)
user={
@@ -455,6 +483,7 @@ exports[`renders in reply to 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": Object {
"author": Object {
"username": "ParentAuthor",
@@ -506,6 +535,7 @@ exports[`renders in reply to 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": Object {
"author": Object {
"username": "ParentAuthor",
@@ -526,6 +556,11 @@ exports[`renders in reply to 1`] = `
parentAuthorName="ParentAuthor"
showEditedMarker={false}
tags={Array []}
topBarRight={
<ForwardRef(forwardRef)
itemGutter={true}
/>
}
username={
<Relay(UsernameWithPopoverContainer)
user={
@@ -571,6 +606,7 @@ exports[`renders staff badge 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -622,6 +658,7 @@ exports[`renders staff badge 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -646,6 +683,11 @@ exports[`renders staff badge 1`] = `
"Staff",
]
}
topBarRight={
<ForwardRef(forwardRef)
itemGutter={true}
/>
}
username={
<Relay(UsernameWithPopoverContainer)
user={
@@ -691,6 +733,7 @@ exports[`renders username and body 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -738,6 +781,7 @@ exports[`renders username and body 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -754,6 +798,11 @@ exports[`renders username and body 1`] = `
parentAuthorName={null}
showEditedMarker={false}
tags={Array []}
topBarRight={
<ForwardRef(forwardRef)
itemGutter={true}
/>
}
username={
<Relay(UsernameWithPopoverContainer)
user={
@@ -799,6 +848,7 @@ exports[`shows conversation link 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -846,6 +896,7 @@ exports[`shows conversation link 1`] = `
"edited": false,
},
"id": "comment-id",
"lastViewerAction": null,
"parent": null,
"pending": false,
"status": "NONE",
@@ -867,6 +918,11 @@ exports[`shows conversation link 1`] = `
parentAuthorName={null}
showEditedMarker={false}
tags={Array []}
topBarRight={
<ForwardRef(forwardRef)
itemGutter={true}
/>
}
username={
<Relay(UsernameWithPopoverContainer)
user={
@@ -11,15 +11,15 @@ exports[`renders correctly 1`] = `
</ForwardRef(forwardRef)>
<Localized
Username={<Username />}
id="comments-inReplyTo"
username={<Username />}
>
<ForwardRef(forwardRef)
className="InReplyTo-inReplyTo"
container="span"
variant="timestamp"
>
In reply to &lt;username&gt;&lt;username&gt;
In reply to &lt;Username&gt;&lt;/Username&gt;
</ForwardRef(forwardRef)>
</Localized>
</ForwardRef(forwardRef)>
@@ -8,8 +8,8 @@ import React, {
import { usePrevious } from "coral-framework/hooks";
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
import { TombstoneOrHideContainer_comment as CommentData } from "coral-stream/__generated__/TombstoneOrHideContainer_comment.graphql";
import { TombstoneOrHideContainer_viewer as ViewerData } from "coral-stream/__generated__/TombstoneOrHideContainer_viewer.graphql";
import { IgnoredTombstoneOrHideContainer_comment as CommentData } from "coral-stream/__generated__/IgnoredTombstoneOrHideContainer_comment.graphql";
import { IgnoredTombstoneOrHideContainer_viewer as ViewerData } from "coral-stream/__generated__/IgnoredTombstoneOrHideContainer_viewer.graphql";
import { CallOut } from "coral-ui/components";
interface Props {
@@ -37,7 +37,7 @@ const useTombstone = (hide: boolean) => {
return tombstone;
};
const TombstoneOrHideContainer: FunctionComponent<Props> = ({
const IgnoredTombstoneOrHideContainer: FunctionComponent<Props> = ({
viewer,
comment,
children,
@@ -70,20 +70,20 @@ const TombstoneOrHideContainer: FunctionComponent<Props> = ({
const enhanced = withFragmentContainer<Props>({
viewer: graphql`
fragment TombstoneOrHideContainer_viewer on User {
fragment IgnoredTombstoneOrHideContainer_viewer on User {
ignoredUsers {
id
}
}
`,
comment: graphql`
fragment TombstoneOrHideContainer_comment on Comment {
fragment IgnoredTombstoneOrHideContainer_comment on Comment {
author {
id
username
}
}
`,
})(TombstoneOrHideContainer);
})(IgnoredTombstoneOrHideContainer);
export default enhanced;
@@ -47,7 +47,7 @@ const enhanced = withFragmentContainer<Props>({
viewer: graphql`
fragment LocalReplyListContainer_viewer on User {
...CommentContainer_viewer
...TombstoneOrHideContainer_viewer
...IgnoredTombstoneOrHideContainer_viewer
}
`,
story: graphql`
@@ -61,7 +61,7 @@ const enhanced = withFragmentContainer<Props>({
localReplies {
id
...CommentContainer_comment
...TombstoneOrHideContainer_comment
...IgnoredTombstoneOrHideContainer_comment
}
}
`,
@@ -6,13 +6,13 @@ import { PropTypesOf } from "coral-framework/types";
import { Button, HorizontalGutter } from "coral-ui/components";
import CommentContainer from "../Comment";
import IgnoredTombstoneOrHideContainer from "../IgnoredTombstoneOrHideContainer";
import Indent from "../Indent";
import TombstoneOrHideContainer from "../TombstoneOrHideContainer";
export interface ReplyListProps {
story: PropTypesOf<typeof CommentContainer>["story"];
viewer: PropTypesOf<typeof CommentContainer>["viewer"] &
PropTypesOf<typeof TombstoneOrHideContainer>["viewer"];
PropTypesOf<typeof IgnoredTombstoneOrHideContainer>["viewer"];
comment: {
id: string;
};
@@ -22,7 +22,7 @@ export interface ReplyListProps {
replyListElement?: React.ReactElement<any>;
showConversationLink?: boolean;
} & PropTypesOf<typeof CommentContainer>["comment"] &
PropTypesOf<typeof TombstoneOrHideContainer>["comment"]
PropTypesOf<typeof IgnoredTombstoneOrHideContainer>["comment"]
>;
settings: PropTypesOf<typeof CommentContainer>["settings"];
onShowAll?: () => void;
@@ -41,7 +41,7 @@ const ReplyList: FunctionComponent<ReplyListProps> = props => {
role="log"
>
{props.comments.map(comment => (
<TombstoneOrHideContainer
<IgnoredTombstoneOrHideContainer
key={comment.id}
viewer={props.viewer}
comment={comment}
@@ -60,7 +60,7 @@ const ReplyList: FunctionComponent<ReplyListProps> = props => {
/>
{comment.replyListElement}
</HorizontalGutter>
</TombstoneOrHideContainer>
</IgnoredTombstoneOrHideContainer>
))}
{props.hasMore && (
<Indent level={props.indentLevel} noBorder>
@@ -22,6 +22,7 @@ it("renders correctly", () => {
replies: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
lastViewerAction: null,
},
settings: {
reaction: {
@@ -51,6 +52,7 @@ it("renders correctly when replies are empty", () => {
id: "comment-id",
status: "NONE",
replies: { edges: [] },
lastViewerAction: null,
},
relay: {
hasMore: noop,
@@ -83,6 +85,7 @@ describe("when has more replies", () => {
replies: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
lastViewerAction: null,
},
settings: {
reaction: {
@@ -53,27 +53,25 @@ export class ReplyListContainer extends React.Component<Props> {
) {
return null;
}
// Don't render replies if comment is not visible.
// We have this comment here because it's
// status just changed due to an edit.
// We keep it around for the CommentContainer to show
// some information to the user.
const comments = !isCommentVisible(this.props.comment)
? []
: this.props.comment.replies.edges.map(edge => ({
...edge.node,
replyListElement: this.props.ReplyListComponent && (
<this.props.ReplyListComponent
viewer={this.props.viewer}
comment={edge.node}
story={this.props.story}
settings={this.props.settings}
/>
),
// ReplyListContainer5 contains replyCount.
showConversationLink:
((edge.node as any) as ReplyNode5).replyCount > 0,
}));
const comments =
// Comment is not visible after a viewer action, so don't render it anymore.
this.props.comment.lastViewerAction &&
!isCommentVisible(this.props.comment)
? []
: this.props.comment.replies.edges.map(edge => ({
...edge.node,
replyListElement: this.props.ReplyListComponent && (
<this.props.ReplyListComponent
viewer={this.props.viewer}
comment={edge.node}
story={this.props.story}
settings={this.props.settings}
/>
),
// ReplyListContainer5 contains replyCount.
showConversationLink:
((edge.node as any) as ReplyNode5).replyCount > 0,
}));
return (
<ReplyList
viewer={this.props.viewer}
@@ -164,7 +162,7 @@ const ReplyListContainer5 = createReplyListContainer(
viewer: graphql`
fragment ReplyListContainer5_viewer on User {
...CommentContainer_viewer
...TombstoneOrHideContainer_viewer
...IgnoredTombstoneOrHideContainer_viewer
...LocalReplyListContainer_viewer
}
`,
@@ -189,6 +187,7 @@ const ReplyListContainer5 = createReplyListContainer(
) {
id
status
lastViewerAction
replies(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "ReplyList_replies") {
edges {
@@ -196,7 +195,7 @@ const ReplyListContainer5 = createReplyListContainer(
id
replyCount
...CommentContainer_comment
...TombstoneOrHideContainer_comment
...IgnoredTombstoneOrHideContainer_comment
...LocalReplyListContainer_comment
}
}
@@ -230,7 +229,7 @@ const ReplyListContainer4 = createReplyListContainer(
fragment ReplyListContainer4_viewer on User {
...ReplyListContainer5_viewer
...CommentContainer_viewer
...TombstoneOrHideContainer_viewer
...IgnoredTombstoneOrHideContainer_viewer
}
`,
settings: graphql`
@@ -254,13 +253,14 @@ const ReplyListContainer4 = createReplyListContainer(
) {
id
status
lastViewerAction
replies(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "ReplyList_replies") {
edges {
node {
id
...CommentContainer_comment
...TombstoneOrHideContainer_comment
...IgnoredTombstoneOrHideContainer_comment
...ReplyListContainer5_comment
}
}
@@ -293,7 +293,7 @@ const ReplyListContainer3 = createReplyListContainer(
fragment ReplyListContainer3_viewer on User {
...ReplyListContainer4_viewer
...CommentContainer_viewer
...TombstoneOrHideContainer_viewer
...IgnoredTombstoneOrHideContainer_viewer
}
`,
settings: graphql`
@@ -317,13 +317,14 @@ const ReplyListContainer3 = createReplyListContainer(
) {
id
status
lastViewerAction
replies(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "ReplyList_replies") {
edges {
node {
id
...CommentContainer_comment
...TombstoneOrHideContainer_comment
...IgnoredTombstoneOrHideContainer_comment
...ReplyListContainer4_comment
}
}
@@ -356,7 +357,7 @@ const ReplyListContainer2 = createReplyListContainer(
fragment ReplyListContainer2_viewer on User {
...ReplyListContainer3_viewer
...CommentContainer_viewer
...TombstoneOrHideContainer_viewer
...IgnoredTombstoneOrHideContainer_viewer
}
`,
settings: graphql`
@@ -380,13 +381,14 @@ const ReplyListContainer2 = createReplyListContainer(
) {
id
status
lastViewerAction
replies(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "ReplyList_replies") {
edges {
node {
id
...CommentContainer_comment
...TombstoneOrHideContainer_comment
...IgnoredTombstoneOrHideContainer_comment
...ReplyListContainer3_comment
}
}
@@ -419,7 +421,7 @@ const ReplyListContainer1 = createReplyListContainer(
fragment ReplyListContainer1_viewer on User {
...ReplyListContainer2_viewer
...CommentContainer_viewer
...TombstoneOrHideContainer_viewer
...IgnoredTombstoneOrHideContainer_viewer
}
`,
settings: graphql`
@@ -443,13 +445,14 @@ const ReplyListContainer1 = createReplyListContainer(
) {
id
status
lastViewerAction
replies(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "ReplyList_replies") {
edges {
node {
id
...CommentContainer_comment
...TombstoneOrHideContainer_comment
...IgnoredTombstoneOrHideContainer_comment
...ReplyListContainer2_comment
}
}
@@ -6,7 +6,7 @@ exports[`renders correctly 1`] = `
id="coral-comments-replyList-log--comment-id"
role="log"
>
<Relay(TombstoneOrHideContainer)
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-1",
@@ -45,8 +45,8 @@ exports[`renders correctly 1`] = `
viewer={null}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
<Relay(TombstoneOrHideContainer)
</Relay(IgnoredTombstoneOrHideContainer)>
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-2",
@@ -87,7 +87,7 @@ exports[`renders correctly 1`] = `
viewer={null}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
</Relay(IgnoredTombstoneOrHideContainer)>
</ForwardRef(forwardRef)>
`;
@@ -97,7 +97,7 @@ exports[`when there is more disables load more button 1`] = `
id="coral-comments-replyList-log--comment-id"
role="log"
>
<Relay(TombstoneOrHideContainer)
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-1",
@@ -134,8 +134,8 @@ exports[`when there is more disables load more button 1`] = `
viewer={null}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
<Relay(TombstoneOrHideContainer)
</Relay(IgnoredTombstoneOrHideContainer)>
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-2",
@@ -172,7 +172,7 @@ exports[`when there is more disables load more button 1`] = `
viewer={null}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
</Relay(IgnoredTombstoneOrHideContainer)>
<Indent
level={1}
noBorder={true}
@@ -201,7 +201,7 @@ exports[`when there is more renders a load more button 1`] = `
id="coral-comments-replyList-log--comment-id"
role="log"
>
<Relay(TombstoneOrHideContainer)
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-1",
@@ -238,8 +238,8 @@ exports[`when there is more renders a load more button 1`] = `
viewer={null}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
<Relay(TombstoneOrHideContainer)
</Relay(IgnoredTombstoneOrHideContainer)>
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-2",
@@ -276,7 +276,7 @@ exports[`when there is more renders a load more button 1`] = `
viewer={null}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
</Relay(IgnoredTombstoneOrHideContainer)>
<Indent
level={1}
noBorder={true}
@@ -5,6 +5,7 @@ exports[`renders correctly 1`] = `
comment={
Object {
"id": "comment-id",
"lastViewerAction": null,
"replies": Object {
"edges": Array [
Object {
@@ -104,6 +105,7 @@ exports[`when has more replies renders hasMore 1`] = `
comment={
Object {
"id": "comment-id",
"lastViewerAction": null,
"replies": Object {
"edges": Array [
Object {
@@ -162,6 +164,7 @@ exports[`when has more replies when showing all disables show all button 1`] = `
comment={
Object {
"id": "comment-id",
"lastViewerAction": null,
"replies": Object {
"edges": Array [
Object {
@@ -220,6 +223,7 @@ exports[`when has more replies when showing all enable show all button after loa
comment={
Object {
"id": "comment-id",
"lastViewerAction": null,
"replies": Object {
"edges": Array [
Object {
@@ -159,7 +159,7 @@ function commit(
},
clientMutationId: (clientMutationId++).toString(),
},
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
},
optimisticUpdater: store => {
// Skip optimistic update if comment is probably premoderated.
if (expectPremoderation) {
@@ -7,8 +7,8 @@ import UserBoxContainer from "coral-stream/common/UserBox";
import { Button, Flex, HorizontalGutter, Spinner } from "coral-ui/components";
import CommentContainer from "../Comment";
import IgnoredTombstoneOrHideContainer from "../IgnoredTombstoneOrHideContainer";
import ReplyListContainer from "../ReplyList";
import TombstoneOrHideContainer from "../TombstoneOrHideContainer";
import BannedInfo from "./BannedInfo";
import CommunityGuidelinesContainer from "./CommunityGuidelines";
import PostCommentFormContainer from "./PostCommentForm";
@@ -35,7 +35,7 @@ export interface StreamProps {
comments: ReadonlyArray<
{ id: string } & PropTypesOf<typeof CommentContainer>["comment"] &
PropTypesOf<typeof ReplyListContainer>["comment"] &
PropTypesOf<typeof TombstoneOrHideContainer>["comment"]
PropTypesOf<typeof IgnoredTombstoneOrHideContainer>["comment"]
>;
onLoadMore?: () => void;
hasMore?: boolean;
@@ -44,7 +44,7 @@ export interface StreamProps {
| PropTypesOf<typeof UserBoxContainer>["viewer"] &
PropTypesOf<typeof CommentContainer>["viewer"] &
PropTypesOf<typeof ReplyListContainer>["viewer"] &
PropTypesOf<typeof TombstoneOrHideContainer>["viewer"]
PropTypesOf<typeof IgnoredTombstoneOrHideContainer>["viewer"]
| null;
orderBy: PropTypesOf<typeof SortMenu>["orderBy"];
onChangeOrderBy: (e: React.ChangeEvent<HTMLSelectElement>) => void;
@@ -84,7 +84,7 @@ const Stream: FunctionComponent<StreamProps> = props => {
aria-live="polite"
>
{props.comments.map(comment => (
<TombstoneOrHideContainer
<IgnoredTombstoneOrHideContainer
key={comment.id}
viewer={props.viewer}
comment={comment}
@@ -103,7 +103,7 @@ const Stream: FunctionComponent<StreamProps> = props => {
story={props.story}
/>
</HorizontalGutter>
</TombstoneOrHideContainer>
</IgnoredTombstoneOrHideContainer>
))}
{props.hasMore && (
<Localized id="comments-stream-loadMore">
@@ -127,7 +127,7 @@ const enhanced = withPaginationContainer<
edges {
node {
...StreamContainer_comment @relay(mask: false)
...TombstoneOrHideContainer_comment
...IgnoredTombstoneOrHideContainer_comment
}
}
}
@@ -146,7 +146,7 @@ const enhanced = withPaginationContainer<
...UserBoxContainer_viewer
...CreateCommentReplyMutation_viewer
...CreateCommentMutation_viewer
...TombstoneOrHideContainer_viewer
...IgnoredTombstoneOrHideContainer_viewer
status {
current
}
@@ -50,7 +50,7 @@ exports[`renders correctly 1`] = `
id="coral-comments-stream-log"
role="log"
>
<Relay(TombstoneOrHideContainer)
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-1",
@@ -103,8 +103,8 @@ exports[`renders correctly 1`] = `
viewer={null}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
<Relay(TombstoneOrHideContainer)
</Relay(IgnoredTombstoneOrHideContainer)>
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-2",
@@ -157,7 +157,7 @@ exports[`renders correctly 1`] = `
viewer={null}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
</Relay(IgnoredTombstoneOrHideContainer)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
`;
@@ -212,7 +212,7 @@ exports[`when there is more disables load more button 1`] = `
id="coral-comments-stream-log"
role="log"
>
<Relay(TombstoneOrHideContainer)
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-1",
@@ -265,8 +265,8 @@ exports[`when there is more disables load more button 1`] = `
viewer={null}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
<Relay(TombstoneOrHideContainer)
</Relay(IgnoredTombstoneOrHideContainer)>
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-2",
@@ -319,7 +319,7 @@ exports[`when there is more disables load more button 1`] = `
viewer={null}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
</Relay(IgnoredTombstoneOrHideContainer)>
<Localized
id="comments-stream-loadMore"
>
@@ -388,7 +388,7 @@ exports[`when there is more renders a load more button 1`] = `
id="coral-comments-stream-log"
role="log"
>
<Relay(TombstoneOrHideContainer)
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-1",
@@ -441,8 +441,8 @@ exports[`when there is more renders a load more button 1`] = `
viewer={null}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
<Relay(TombstoneOrHideContainer)
</Relay(IgnoredTombstoneOrHideContainer)>
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-2",
@@ -495,7 +495,7 @@ exports[`when there is more renders a load more button 1`] = `
viewer={null}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
</Relay(IgnoredTombstoneOrHideContainer)>
<Localized
id="comments-stream-loadMore"
>
@@ -564,7 +564,7 @@ exports[`when use is logged in renders correctly 1`] = `
id="coral-comments-stream-log"
role="log"
>
<Relay(TombstoneOrHideContainer)
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-1",
@@ -617,8 +617,8 @@ exports[`when use is logged in renders correctly 1`] = `
viewer={Object {}}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
<Relay(TombstoneOrHideContainer)
</Relay(IgnoredTombstoneOrHideContainer)>
<Relay(IgnoredTombstoneOrHideContainer)
comment={
Object {
"id": "comment-2",
@@ -671,7 +671,7 @@ exports[`when use is logged in renders correctly 1`] = `
viewer={Object {}}
/>
</ForwardRef(forwardRef)>
</Relay(TombstoneOrHideContainer)>
</Relay(IgnoredTombstoneOrHideContainer)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
`;
@@ -1,7 +1,7 @@
import React, { FunctionComponent, useCallback } from "react";
import { PropTypesOf } from "coral-framework/types";
import { ProfileLocal as Local } from "coral-stream/__generated__/ProfileLocal.graphql";
import { ProfileLocal } from "coral-stream/__generated__/ProfileLocal.graphql";
import UserBoxContainer from "coral-stream/common/UserBox";
import {
HorizontalGutter,
@@ -24,13 +24,13 @@ export interface ProfileProps {
}
const Profile: FunctionComponent<ProfileProps> = props => {
const [local, setLocal] = useLocal<Local>(graphql`
const [local, setLocal] = useLocal<ProfileLocal>(graphql`
fragment ProfileLocal on Local {
profileTab
}
`);
const onTabClick = useCallback(
(tab: Local["profileTab"]) => setLocal({ profileTab: tab }),
(tab: ProfileLocal["profileTab"]) => setLocal({ profileTab: tab }),
[setLocal]
);
return (
@@ -170,6 +170,11 @@ exports[`renders permalink view 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -371,6 +376,11 @@ exports[`renders permalink view 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -570,6 +580,11 @@ exports[`renders permalink view 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -765,6 +780,11 @@ exports[`renders permalink view 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -949,6 +969,11 @@ exports[`renders permalink view 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -204,6 +204,11 @@ exports[`renders conversation thread 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -418,6 +423,11 @@ exports[`shows more of this conversation 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -624,6 +634,11 @@ exports[`shows more of this conversation 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -823,6 +838,11 @@ exports[`shows more of this conversation 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -74,18 +74,22 @@ exports[`cancel edit 1`] = `
</div>
</div>
<div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
Edit
</button>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
</div>
</div>
<div
@@ -805,18 +809,22 @@ exports[`edit a comment: render comment with edit button 1`] = `
</div>
</div>
<div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
Edit
</button>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
</div>
</div>
<div
@@ -1029,18 +1037,22 @@ exports[`edit a comment: server response 1`] = `
</div>
</div>
<div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
Edit
</button>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
</div>
</div>
<div
@@ -1244,18 +1256,22 @@ exports[`shows expiry message: edit form closed 1`] = `
</div>
</div>
<div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
Edit
</button>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
</div>
</div>
<div
@@ -83,6 +83,11 @@ exports[`renders comment stream with load more button 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -267,6 +272,11 @@ exports[`renders comment stream with load more button 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -74,18 +74,22 @@ exports[`post a comment: optimistic response 1`] = `
</div>
</div>
<div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
Edit
</button>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
</div>
</div>
<div
@@ -73,6 +73,11 @@ exports[`post a reply: open reply form 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -455,18 +460,22 @@ exports[`post a reply: optimistic response 1`] = `
</div>
</div>
<div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
Edit
</button>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
</div>
</div>
<div
@@ -665,6 +674,11 @@ exports[`renders comment stream 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -872,6 +886,11 @@ exports[`renders comment stream 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -1079,6 +1098,11 @@ exports[`renders comment stream 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -1286,6 +1310,11 @@ exports[`renders comment stream 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -1493,6 +1522,11 @@ exports[`renders comment stream 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -1700,6 +1734,11 @@ exports[`renders comment stream 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -73,6 +73,11 @@ exports[`post a reply: open reply form 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -440,18 +445,22 @@ exports[`post a reply: optimistic response 1`] = `
</div>
</div>
<div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
Edit
</button>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
</div>
</div>
<div
@@ -295,6 +295,11 @@ exports[`renders comment stream with community guidelines 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -479,6 +484,11 @@ exports[`renders comment stream with community guidelines 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -82,6 +82,11 @@ exports[`renders reply list 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -271,6 +276,11 @@ exports[`renders reply list 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -455,6 +465,11 @@ exports[`renders reply list 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -641,6 +656,11 @@ exports[`renders reply list 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -313,6 +313,11 @@ exports[`renders comment stream 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -502,6 +507,11 @@ exports[`renders comment stream 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -82,6 +82,11 @@ exports[`renders comment stream 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -73,6 +73,11 @@ exports[`renders deepest comment with link 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -83,6 +83,11 @@ exports[`renders app with comment stream 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -267,6 +272,11 @@ exports[`renders app with comment stream 1`] = `
</time>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter"
/>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -0,0 +1,135 @@
import { pureMerge } from "coral-common/utils";
import { GQLCOMMENT_STATUS, GQLResolver } from "coral-framework/schema";
import {
createResolversStub,
CreateTestRendererParams,
waitForElement,
within,
} from "coral-framework/testHelpers";
import { moderators, settings, stories } from "../../fixtures";
import create from "../create";
const story = stories[0];
const firstComment = story.comments.edges[0].node;
const viewer = moderators[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
story: () => story,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(story.id, "storyID");
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const tabPane = await waitForElement(() =>
within(testRenderer.root).getByTestID("current-tab-pane")
);
return {
testRenderer,
context,
tabPane,
};
}
it("render go to moderate link", async () => {
const { testRenderer } = await createTestRenderer();
const comment = await waitForElement(() =>
within(testRenderer.root).getByTestID(`comment-${firstComment.id}`)
);
const caretButton = within(comment).getByLabelText("Moderate");
caretButton.props.onClick();
const link = within(comment).getByText("Go to Moderate", {
selector: "a",
exact: false,
});
expect(link.props.href).toBe(`/admin/moderate/comment/${firstComment.id}`);
});
it("accept comment", async () => {
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Mutation: {
acceptComment: ({ variables }) => {
expectAndFail(variables).toMatchObject({
commentID: firstComment.id,
commentRevisionID: firstComment.revision.id,
});
return {
comment: pureMerge<typeof firstComment>(firstComment, {
status: GQLCOMMENT_STATUS.ACCEPTED,
}),
};
},
},
}),
});
const comment = await waitForElement(() =>
within(testRenderer.root).getByTestID(`comment-${firstComment.id}`)
);
const caretButton = within(comment).getByLabelText("Moderate");
caretButton.props.onClick();
const approveButton = within(comment).getByText("Approve", {
selector: "button",
});
approveButton.props.onClick();
await waitForElement(() =>
within(comment).getByText("Approved", { exact: false })
);
});
it("reject comment", async () => {
const { testRenderer, tabPane } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Mutation: {
rejectComment: ({ variables }) => {
expectAndFail(variables).toMatchObject({
commentID: firstComment.id,
commentRevisionID: firstComment.revision.id,
});
return {
comment: pureMerge<typeof firstComment>(firstComment, {
status: GQLCOMMENT_STATUS.REJECTED,
}),
};
},
},
}),
});
const comment = await waitForElement(() =>
within(testRenderer.root).getByTestID(`comment-${firstComment.id}`)
);
const caretButton = within(comment).getByLabelText("Moderate");
caretButton.props.onClick();
const rejectButton = within(comment).getByText("Reject", {
selector: "button",
});
rejectButton.props.onClick();
await waitForElement(() =>
within(tabPane).getByText("You have rejected this comment", {
exact: false,
})
);
const link = within(tabPane).getByText(
"Go to Moderate to review this decision",
{ selector: "a", exact: false }
);
expect(link.props.href).toBe(`/admin/moderate/comment/${firstComment.id}`);
});
@@ -3,25 +3,46 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: calc(0.5 * var(--mini-unit)) var(--mini-unit);
padding: var(--spacing-2);
width: 100%;
text-align: left;
line-height: 1;
}
.root:active {
background-color: var(--palette-grey-lighter);
.anchor {
color: var(--palette-primary-main);
text-decoration: underline;
&:visited {
color: var(--palette-primary-main);
}
}
.root:not(:disabled):active {
background-color: var(--palette-primary-lightest);
}
.mouseHover {
background-color: var(--palette-grey-lightest);
background-color: var(--palette-primary-lightest);
}
.icon {
.iconBefore {
display: flex;
justify-content: baseline;
align-items: center;
color: var(--palette-grey-main);
margin-left: var(--mini-unit);
margin-right: var(--spacing-2);
padding-top: 1px;
}
.iconAfter {
display: flex;
align-items: center;
justify-content: baseline;
margin-left: var(--spacing-2);
padding-top: 1px;
color: var(--palette-primary-main);
text-decoration: unset;
}
.blankAdornment {
padding-right: calc(3.5 * var(--mini-unit));
padding-right: var(--spacing-6);
}
@@ -7,10 +7,12 @@ import { withStyles } from "coral-ui/hocs";
import BaseButton, { BaseButtonProps } from "../BaseButton";
import Icon from "../Icon";
import { Flex } from "..";
import styles from "./Button.css";
interface Props extends Omit<BaseButtonProps, "ref"> {
children: React.ReactNode;
icon?: React.ReactNode;
href?: string;
className?: string;
onClick?: React.EventHandler<React.MouseEvent>;
@@ -30,6 +32,8 @@ const Button: FunctionComponent<Props> = ({
onClick,
children,
classes,
icon,
disabled,
...rest
}) => {
return (
@@ -38,16 +42,26 @@ const Button: FunctionComponent<Props> = ({
root: cn(classes.root, className, {
[classes.blankAdornment]: blankAdornment,
}),
mouseHover: classes.mouseHover,
mouseHover: cn({ [classes.mouseHover]: !disabled }),
}}
href={href}
onClick={onClick}
anchor={Boolean(href)}
disabled={disabled}
{...rest}
>
{children}
<Flex>
{icon && <div className={classes.iconBefore}>{icon}</div>}
<div
className={cn({
[classes.anchor]: Boolean(href),
})}
>
{children}
</div>
</Flex>
{rest.target === "_blank" && (
<Icon className={classes.icon}>open_in_new</Icon>
<Icon className={classes.iconAfter}>open_in_new</Icon>
)}
</BaseButton>
);
@@ -1,6 +1,5 @@
.root {
border: 0;
border-bottom: 1px solid var(--palette-divider);
padding-top: 1px;
margin: calc(0.5 * var(--mini-unit)) 0;
margin: 0;
}
@@ -5,8 +5,10 @@ exports[`renders anchor button 1`] = `
className="custom"
classes={
Object {
"anchor": "Button-anchor",
"blankAdornment": "Button-blankAdornment",
"icon": "Button-icon",
"iconAfter": "Button-iconAfter",
"iconBefore": "Button-iconBefore",
"mouseHover": "Button-mouseHover",
"root": "Button-root",
}
@@ -25,8 +27,10 @@ exports[`renders button 1`] = `
className="custom"
classes={
Object {
"anchor": "Button-anchor",
"blankAdornment": "Button-blankAdornment",
"icon": "Button-icon",
"iconAfter": "Button-iconAfter",
"iconBefore": "Button-iconBefore",
"mouseHover": "Button-mouseHover",
"root": "Button-root",
}
@@ -8,6 +8,7 @@
box-shadow: var(--elevation-main);
border-radius: var(--round-corners);
z-index: var(--zindex-popover);
margin: 2px;
}
.top {
@@ -7,8 +7,6 @@ import { withStyles } from "coral-ui/hocs";
import styles from "./TextLink.css";
interface Props extends AnchorHTMLAttributes<HTMLAnchorElement> {
/** external if set to true will show a little icon that indicates an external link */
external?: boolean;
/**
* Override or extend the styles applied to the component.
*/
@@ -16,7 +14,7 @@ interface Props extends AnchorHTMLAttributes<HTMLAnchorElement> {
}
const TextLinkProps: StatelessComponent<Props> = props => {
const { className, children, classes, external, ...rest } = props;
const { className, children, classes, target, ...rest } = props;
const rootClassName = cn(classes.root, className);
@@ -24,12 +22,13 @@ const TextLinkProps: StatelessComponent<Props> = props => {
<a
className={rootClassName}
href={props.href || (children as string)}
target={target}
{...rest}
>
{children}
{external && (
<Icon className={styles.icon} size="xs">
launch
{target === "_blank" && (
<Icon className={classes.icon} size="xs">
open_in_new
</Icon>
)}
</a>
+7 -7
View File
@@ -39,7 +39,7 @@ userMenu-popover =
## Restricted
restricted-currentlySignedInTo = Currently signed in to
restricted-noPermissionInfo = You do not have permission to access this page.
restricted-signedInAs = You are signed in as: <username></username>
restricted-signedInAs = You are signed in as: <Username></Username>
restricted-signInWithADifferentAccount = Sign in with a different account
restricted-contactAdmin = If you think this is an error, please contact your administrator for assistance.
@@ -176,14 +176,14 @@ configure-auth-facebook-loginWith = Login with Facebook
configure-auth-facebook-toEnableIntegration =
To enable the integration with Facebook Authentication,
you need to create and set up a web application.
For more information visit: <link></link>.
For more information visit: <Link></Link>.
configure-auth-facebook-useLoginOn = Use Facebook login on
configure-auth-google-loginWith = Login with Google
configure-auth-google-toEnableIntegration =
To enable the integration with Google Authentication you need
to create and set up a web application. For more information visit:
<link></link>.
<Link></Link>.
configure-auth-google-useLoginOn = Use Google login on
configure-auth-sso-loginWith = Login with Single Sign On
@@ -200,7 +200,7 @@ configure-auth-local-loginWith = Login with Email Authentication
configure-auth-local-useLoginOn = Use Email Authentication login on
configure-auth-oidc-loginWith = Login with OpenID Connect
configure-auth-oidc-toLearnMore = To learn more: <link></link>
configure-auth-oidc-toLearnMore = To learn more: <Link></Link>
configure-auth-oidc-providerName = Provider Name
configure-auth-oidc-providerNameDescription =
The provider of the OpenID Connect integration. This will be used when the name of the provider
@@ -297,8 +297,8 @@ decisionHistory-youWillSeeAList =
decisionHistory-showMoreButton =
Show More
decisionHistory-yourDecisionHistory = Your Decision History
decisionHistory-rejectedCommentBy = Rejected comment by <username></username>
decisionHistory-acceptedCommentBy = Accepted comment by <username></username>
decisionHistory-rejectedCommentBy = Rejected comment by <Username></Username>
decisionHistory-acceptedCommentBy = Accepted comment by <Username></Username>
decisionHistory-goToComment = Go to comment
## moderate
@@ -326,7 +326,7 @@ moderate-emptyQueue-reported = Nicely done! There are no more reported comments
moderate-emptyQueue-unmoderated = Nicely done! All comments have been moderated.
moderate-emptyQueue-rejected = There are no rejected comments.
moderate-comment-inReplyTo = Reply to <username><username>
moderate-comment-inReplyTo = Reply to <Username></Username>
moderate-comment-viewContext = View Context
moderate-comment-rejectButton =
.aria-label = Reject
+16 -3
View File
@@ -7,7 +7,7 @@ general-userBoxUnauthenticated-signIn = Sign in
general-userBoxUnauthenticated-register = Register
general-userBoxAuthenticated-signedInAs =
Signed in as <username></username>.
Signed in as <Username></Username>.
general-userBoxAuthenticated-notYou =
Not you? <button>Sign Out</button>
@@ -82,8 +82,8 @@ comments-conversationThread-showMoreOfThisConversation =
comments-permalinkView-currentViewing = You are currently viewing a
comments-permalinkView-singleConversation = SINGLE CONVERSATION
comments-inReplyTo = In reply to <username></username>
comments-replyTo = Replying to: <username></username>
comments-inReplyTo = In reply to <Username></Username>
comments-replyTo = Replying to: <Username></Username>
comments-reportButton-report = Report
comments-reportButton-reported = Reported
@@ -106,6 +106,19 @@ comments-userIgnorePopover-description =
comments-userIgnorePopover-ignore = Ignore
comments-userIgnorePopover-cancel = Cancel
comments-moderationDropdown-popover =
.description = A popover menu to moderate the comment
comments-moderationDropdown-approve = Approve
comments-moderationDropdown-approved = Approved
comments-moderationDropdown-reject = Reject
comments-moderationDropdown-rejected = Rejected
comments-moderationDropdown-goToModerate = Go to Moderate
comments-moderationDropdown-caretButton =
.aria-label = Moderate
comments-rejectedTombstone =
You have rejected this comment. <TextLink>Go to Moderate to review this decision.</TextLink>
## Profile Tab
### Comment History