[CORL-233] Create a user drawer with comment streams (#2395)

* Create preliminary user history drawer with user fragment

CORL-233

* Set font size for the user details to match spec at 16 px

CORL-233

* Update username font style/size, add horizontal rule for user history drawer

CORL-233

* Set user detail min width to align the copy buttons in user history drawer

CORL-233

* Set min width of the user history drawer to match spec

CORL-233

* Remove min width for detail area and card to allow responsive sizing

Allows the copy buttons to sit just to the right of the user
details text.

CORL-233

* Update snapshots to handle clickable username in moderation comment cards

CORL-233

* Create initial pagination of all comments in user history drawer

Shows all comments with a rubbish "load more" button as we haven't
decided if we want to do a load more button or infinite scrolling
solution.

Flex layout helps it create a scroll bar when needed to traverse through
the comment stream.

CORL-233

* Create preliminary user history drawer with user fragment

CORL-233

* Set font size for the user details to match spec at 16 px

CORL-233

* Update username font style/size, add horizontal rule for user history drawer

CORL-233

* Set user detail min width to align the copy buttons in user history drawer

CORL-233

* Set min width of the user history drawer to match spec

CORL-233

* Remove min width for detail area and card to allow responsive sizing

Allows the copy buttons to sit just to the right of the user
details text.

CORL-233

* Update snapshots to handle clickable username in moderation comment cards

CORL-233

* Let the user history drawer fill full height of page

CORL-233

* Add a close button to the user history drawer

CORL-233

* Preliminarily get all and rejected comments showing in the user drawer

CORL-233

* Create preliminary user history drawer with user fragment

CORL-233

* Set font size for the user details to match spec at 16 px

CORL-233

* Update username font style/size, add horizontal rule for user history drawer

CORL-233

* Set user detail min width to align the copy buttons in user history drawer

CORL-233

* Set min width of the user history drawer to match spec

CORL-233

* Remove min width for detail area and card to allow responsive sizing

Allows the copy buttons to sit just to the right of the user
details text.

CORL-233

* Update snapshots to handle clickable username in moderation comment cards

CORL-233

* Let the user history drawer fill full height of page

CORL-233

* Add a close button to the user history drawer

CORL-233

* Set the user history drawer to fill full height of window

CORL-233

* Convert ModerateCardContainer to a FunctionComponent

CORL-233

* Show spinner while all comments are loading in user history drawer

CORL-233

* Style tabs within the user history drawer to be secondary

CORL-233

* Center load more button in the comments for the user history drawer

CORL-233

* Fix scroll bars on nested flexboxes in the user history drawer

CORL-233

* Tweak styling for the user history drawer comments section

Add margins where necessary.
Style the Load More button to match the comment feed elsewhere.

CORL-233

* Fix background of copy buttons to match the user history drawer

CORL-233

* Preliminarily get rejected comments showing in the user history tabs

Still need to consolidate any duplication between the all and rejected
comments containers/queries, but this works correctly as is.

CORL-233

* Show hover and active background color on moderate card usernames

CORL-233

* Hide load more buttons on comment streams when no comments available

CORL-233

* Handle when no comments are available in user history drawer streams

CORL-233

* Actually show all comments in the user history all comments tab

Previously was filtering out rejected and other pre-mod
status-ed comments.

CORL-233

* Style the user history drawer cards to match the spec

CORL-233

* Add localizations around user history drawer tabs

CORL-233

* Allow a mini mode for moderate cards that are shown in the user drawer

CORL-233

* Style user drawer comment tabs to match the spec

CORL-233

* Update snapshots to account for mini moderate cards

Updates the fact that a separator on regular sized cards
now has an additional class name to turn on the divider's
visible border.

CORL-233

* Add user drawer to the queue instead of childed to the moderate card

Prevents the user drawer from prematurely closing when we approve
all items in the moderate queue.

CORL-233

* Update snapshots to account for user drawer on queues

CORL-233

* Set mini and username defaults more cleanly on ModerateCardContainer

CORL-233

* Rename usernameClicked to onUsernameClicked

CORL-233

* Use callback on loadMore calls within user drawer queries

CORL-233

* Localize the no comment text for the user drawer comment streams

CORL-233

* Localize the user not found callout in the user drawer comment streams

CORL-233

* Remove superfluous curly braces

CORL-233

* Extract user history drawer internals into a distinct query component

CORL-233

* Move comments conditional below useCallback initializations

CORL-233

* feat: fixed consistency issues

* Use a concrete tab type on user drawer comment stream tabs

CORL-233

* Use <hr/> instead of <HorizontalRule>

CORL-233

* Clarify logic for showing horizontal rule after comments in user drawer

CORL-233

* Remover bottom border on user drawer tab bar

CORL-233
This commit is contained in:
Nick Funk
2019-07-10 10:59:28 -06:00
committed by GitHub
parent 9e55ea66fa
commit 0b20542f05
31 changed files with 1652 additions and 242 deletions
@@ -1,9 +1,15 @@
import cn from "classnames";
import React, { FunctionComponent } from "react";
import styles from "./HorizontalRule.css";
const HorizontalRule: FunctionComponent = ({ children }) => (
<hr className={styles.root} />
interface Props {
className?: string;
children?: any;
}
const HorizontalRule: FunctionComponent<Props> = ({ children, className }) => (
<hr className={cn(styles.root, className)} />
);
export default HorizontalRule;
@@ -1,8 +1,11 @@
.root {
position: absolute;
right: 0px;
top: 2px;
border: 1px solid var(--palette-grey-lighter);
box-sizing: border-box;
border-radius: var(--round-corners);
float: right;
padding: 2px var(--spacing-2) 2px var(--spacing-2);
display: flex;
justify-content: center;
@@ -1,9 +1,29 @@
.topBar {
position: relative;
margin-bottom: var(--mini-unit);
}
.topBarMini {
margin-bottom: var(--spacing-4);
}
.username {
margin-right: var(--mini-unit);
padding: var(--spacing-1);
margin-left: calc(-1 * var(--spacing-1));
line-height: calc(16rem / var(--rem-base));
&:hover {
background-color: var(--palette-grey-lightest);
border-radius: 2px;
border-style: none;
}
&:active {
background-color: var(--palette-grey-lighter);
border-radius: 2px;
border-style: none;
}
}
.footer {
@@ -19,16 +39,32 @@
}
.aside {
padding-top: 25px;
flex-shrink: 0;
flex-grow: 0;
box-sizing: border-box;
padding-top: 25px;
}
.asideWithoutReplyTo {
padding-top: 10px;
}
.asideMini {
/*
No reply to field, so we align below the fattest element
which is the feature button.
*/
padding-top: calc((14rem / var(--rem-base)) + 7px + var(--spacing-4));
}
.asideMiniWithReplyTo {
/*
We have reply to field, so we align below the fattest
element plus the reply to field.
*/
padding-top: calc(2 * (14rem / var(--rem-base)) + 5px + var(--spacing-4));
}
.decision {
font-size: calc(14rem / var(--rem-base));
font-weight: var(--font-weight-medium);
@@ -42,10 +78,13 @@
.separator {
flex-shrink: 0;
flex-grow: 0;
border-right: 1px solid var(--palette-divider);
margin: 0 calc(2 * var(--mini-unit));
}
.ruledSeparator {
border-right: 1px solid var(--palette-divider);
}
.root {
transition: background 100ms, box-shadow 100ms;
}
@@ -103,3 +142,13 @@
text-align: center;
padding-top: 1px;
}
.borderless {
border-width: 0px;
box-shadow: none;
}
.miniButton {
width: 40px;
height: 40px;
}
@@ -24,6 +24,7 @@ const baseProps: PropTypesOf<typeof ModerateCardN> = {
onApprove: noop,
onReject: noop,
onFeature: noop,
onUsernameClick: noop,
showStory: false,
moderatedBy: null,
};
@@ -3,7 +3,13 @@ import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { PropTypesOf } from "coral-framework/types";
import { Card, Flex, HorizontalGutter, TextLink } from "coral-ui/components";
import {
BaseButton,
Card,
Flex,
HorizontalGutter,
TextLink,
} from "coral-ui/components";
import ApproveButton from "./ApproveButton";
import CommentContent from "./CommentContent";
@@ -36,6 +42,9 @@ interface Props {
onApprove: () => void;
onReject: () => void;
onFeature: () => void;
onUsernameClick: () => void;
mini?: boolean;
hideUsername?: boolean;
/**
* If set to true, it means this comment is about to be removed
* from the queue. This will trigger some styling changes to
@@ -59,25 +68,40 @@ const ModerateCard: FunctionComponent<Props> = ({
onApprove,
onReject,
onFeature,
onUsernameClick,
dangling,
showStory,
storyTitle,
storyHref,
onModerateStory,
moderatedBy,
mini = false,
hideUsername = false,
}) => (
<Card
className={cn(styles.root, { [styles.dangling]: dangling })}
className={cn(
styles.root,
{ [styles.borderless]: mini },
{ [styles.dangling]: dangling }
)}
data-testid={`moderate-comment-${id}`}
>
<Flex>
<div className={styles.mainContainer}>
<div className={styles.topBar}>
<div>
<Username className={styles.username}>{username}</Username>
<div
className={cn(styles.topBar, {
[styles.topBarMini]: mini && !inReplyTo,
})}
>
<Flex alignItems="center">
{!hideUsername && (
<BaseButton onClick={onUsernameClick} className={styles.username}>
<Username>{username}</Username>
</BaseButton>
)}
<Timestamp>{createdAt}</Timestamp>
<FeatureButton featured={featured} onClick={onFeature} />
</div>
</Flex>
{inReplyTo && (
<div>
<InReplyTo>{inReplyTo}</InReplyTo>
@@ -126,28 +150,36 @@ const ModerateCard: FunctionComponent<Props> = ({
</HorizontalGutter>
</div>
</div>
<div className={styles.separator} />
<div
className={cn(styles.separator, { [styles.ruledSeparator]: !mini })}
/>
<Flex
className={cn(styles.aside, {
[styles.asideWithoutReplyTo]: !inReplyTo,
[styles.asideMini]: mini && !inReplyTo,
[styles.asideMiniWithReplyTo]: mini && inReplyTo,
})}
alignItems="center"
direction="column"
itemGutter
>
<Localized id="moderate-comment-decision">
<div className={styles.decision}>DECISION</div>
</Localized>
{!mini && (
<Localized id="moderate-comment-decision">
<div className={styles.decision}>DECISION</div>
</Localized>
)}
<Flex itemGutter>
<RejectButton
onClick={onReject}
invert={status === "rejected"}
disabled={status === "rejected" || dangling}
className={cn({ [styles.miniButton]: mini })}
/>
<ApproveButton
onClick={onApprove}
invert={status === "approved"}
disabled={status === "approved" || dangling}
className={cn({ [styles.miniButton]: mini })}
/>
</Flex>
{moderatedBy}
@@ -1,5 +1,5 @@
import { Match, Router, withRouter } from "found";
import React from "react";
import React, { FunctionComponent, useCallback } from "react";
import { graphql } from "react-relay";
import {
@@ -37,6 +37,9 @@ interface Props {
match: Match;
router: Router;
showStoryInfo: boolean;
mini?: boolean;
hideUsername?: boolean;
onUsernameClicked?: (userID: string) => void;
}
function getStatus(comment: ModerateCardContainer_comment) {
@@ -54,67 +57,82 @@ function isFeatured(comment: ModerateCardContainer_comment) {
return comment.tags.some(t => t.code === GQLTAG.FEATURED);
}
class ModerateCardContainer extends React.Component<Props> {
private handleApprove = () => {
this.props.approveComment({
commentID: this.props.comment.id,
commentRevisionID: this.props.comment.revision.id,
storyID: this.props.match.params.storyID,
const ModerateCardContainer: FunctionComponent<Props> = ({
comment,
settings,
danglingLogic,
showStoryInfo,
viewer,
match,
router,
approveComment,
rejectComment,
featureComment,
unfeatureComment,
mini,
hideUsername,
onUsernameClicked: usernameClicked,
}) => {
const handleApprove = useCallback(() => {
approveComment({
commentID: comment.id,
commentRevisionID: comment.revision.id,
storyID: match.params.storyID,
});
};
}, [approveComment, comment, match]);
private handleReject = () => {
this.props.rejectComment({
commentID: this.props.comment.id,
commentRevisionID: this.props.comment.revision.id,
storyID: this.props.match.params.storyID,
const handleReject = useCallback(() => {
rejectComment({
commentID: comment.id,
commentRevisionID: comment.revision.id,
storyID: match.params.storyID,
});
};
}, [rejectComment, comment, match]);
private onFeature = () => {
const featured = isFeatured(this.props.comment);
const handleFeature = useCallback(() => {
featureComment({
commentID: comment.id,
commentRevisionID: comment.revision.id,
storyID: match.params.storyID,
});
}, [featureComment, comment, match]);
const handleUnfeature = useCallback(() => {
unfeatureComment({
commentID: comment.id,
storyID: match.params.storyID,
});
}, [unfeatureComment, comment, match]);
const onFeature = useCallback(() => {
const featured = isFeatured(comment);
if (featured) {
this.handleUnfeature();
handleUnfeature();
} else {
this.handleFeature();
handleFeature();
}
};
}, [comment]);
private handleFeature = () => {
this.props.featureComment({
commentID: this.props.comment.id,
commentRevisionID: this.props.comment.revision.id,
storyID: this.props.match.params.storyID,
});
};
private handleUnfeature = () => {
this.props.unfeatureComment({
commentID: this.props.comment.id,
storyID: this.props.match.params.storyID,
});
};
private handleModerateStory = (e: React.MouseEvent) => {
this.props.router.push(
getModerationLink("default", this.props.comment.story.id)
);
if (e.preventDefault) {
e.preventDefault();
const onUsernameClicked = useCallback(() => {
if (!usernameClicked) {
return;
}
};
usernameClicked(comment.author!.id);
}, [usernameClicked, comment]);
public render() {
const {
comment,
settings,
danglingLogic,
showStoryInfo,
viewer,
} = this.props;
const dangling = danglingLogic(comment.status);
return (
const handleModerateStory = useCallback(
(e: React.MouseEvent) => {
router.push(getModerationLink("default", comment.story.id));
if (e.preventDefault) {
e.preventDefault();
}
},
[router, comment]
);
return (
<>
<FadeInTransition active={Boolean(comment.enteredLive)}>
<ModerateCard
id={comment.id}
@@ -123,15 +141,16 @@ class ModerateCardContainer extends React.Component<Props> {
body={comment.body!}
inReplyTo={comment.parent && comment.parent.author!.username!}
comment={comment}
dangling={dangling}
dangling={danglingLogic(comment.status)}
status={getStatus(comment)}
featured={isFeatured(comment)}
viewContextHref={comment.permalink}
suspectWords={settings.wordList.suspect}
bannedWords={settings.wordList.banned}
onApprove={this.handleApprove}
onReject={this.handleReject}
onFeature={this.onFeature}
onApprove={handleApprove}
onReject={handleReject}
onFeature={onFeature}
onUsernameClick={onUsernameClicked}
moderatedBy={
<ModeratedByContainer viewer={viewer} comment={comment} />
}
@@ -142,18 +161,21 @@ class ModerateCardContainer extends React.Component<Props> {
)
}
storyHref={getModerationLink("default", comment.story.id)}
onModerateStory={this.handleModerateStory}
onModerateStory={handleModerateStory}
mini={mini}
hideUsername={hideUsername}
/>
</FadeInTransition>
);
}
}
</>
);
};
const enhanced = withFragmentContainer<Props>({
comment: graphql`
fragment ModerateCardContainer_comment on Comment {
id
author {
id
username
}
statusLiveUpdated
@@ -12,12 +12,17 @@ exports[`renders approved correctly 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<Username
<ForwardRef(forwardRef)
alignItems="center"
>
<ForwardRef(forwardRef)
className="ModerateCard-username"
onClick={[Function]}
>
Theon
</Username>
<Username>
Theon
</Username>
</ForwardRef(forwardRef)>
<Timestamp>
2018-11-29T16:01:51.897Z
</Timestamp>
@@ -25,7 +30,7 @@ exports[`renders approved correctly 1`] = `
featured={false}
onClick={[Function]}
/>
</div>
</ForwardRef(forwardRef)>
</div>
<CommentContent
bannedWords={
@@ -66,7 +71,7 @@ exports[`renders approved correctly 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<ForwardRef(forwardRef)
alignItems="center"
@@ -87,10 +92,12 @@ exports[`renders approved correctly 1`] = `
itemGutter={true}
>
<RejectButton
className=""
invert={false}
onClick={[Function]}
/>
<ApproveButton
className=""
disabled={true}
invert={true}
onClick={[Function]}
@@ -113,12 +120,17 @@ exports[`renders correctly 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<Username
<ForwardRef(forwardRef)
alignItems="center"
>
<ForwardRef(forwardRef)
className="ModerateCard-username"
onClick={[Function]}
>
Theon
</Username>
<Username>
Theon
</Username>
</ForwardRef(forwardRef)>
<Timestamp>
2018-11-29T16:01:51.897Z
</Timestamp>
@@ -126,7 +138,7 @@ exports[`renders correctly 1`] = `
featured={false}
onClick={[Function]}
/>
</div>
</ForwardRef(forwardRef)>
</div>
<CommentContent
bannedWords={
@@ -167,7 +179,7 @@ exports[`renders correctly 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<ForwardRef(forwardRef)
alignItems="center"
@@ -188,10 +200,12 @@ exports[`renders correctly 1`] = `
itemGutter={true}
>
<RejectButton
className=""
invert={false}
onClick={[Function]}
/>
<ApproveButton
className=""
invert={false}
onClick={[Function]}
/>
@@ -213,12 +227,17 @@ exports[`renders dangling correctly 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<Username
<ForwardRef(forwardRef)
alignItems="center"
>
<ForwardRef(forwardRef)
className="ModerateCard-username"
onClick={[Function]}
>
Theon
</Username>
<Username>
Theon
</Username>
</ForwardRef(forwardRef)>
<Timestamp>
2018-11-29T16:01:51.897Z
</Timestamp>
@@ -226,7 +245,7 @@ exports[`renders dangling correctly 1`] = `
featured={false}
onClick={[Function]}
/>
</div>
</ForwardRef(forwardRef)>
</div>
<CommentContent
bannedWords={
@@ -267,7 +286,7 @@ exports[`renders dangling correctly 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<ForwardRef(forwardRef)
alignItems="center"
@@ -288,11 +307,13 @@ exports[`renders dangling correctly 1`] = `
itemGutter={true}
>
<RejectButton
className=""
disabled={true}
invert={false}
onClick={[Function]}
/>
<ApproveButton
className=""
disabled={true}
invert={false}
onClick={[Function]}
@@ -315,12 +336,17 @@ exports[`renders rejected correctly 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<Username
<ForwardRef(forwardRef)
alignItems="center"
>
<ForwardRef(forwardRef)
className="ModerateCard-username"
onClick={[Function]}
>
Theon
</Username>
<Username>
Theon
</Username>
</ForwardRef(forwardRef)>
<Timestamp>
2018-11-29T16:01:51.897Z
</Timestamp>
@@ -328,7 +354,7 @@ exports[`renders rejected correctly 1`] = `
featured={false}
onClick={[Function]}
/>
</div>
</ForwardRef(forwardRef)>
</div>
<CommentContent
bannedWords={
@@ -369,7 +395,7 @@ exports[`renders rejected correctly 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<ForwardRef(forwardRef)
alignItems="center"
@@ -390,11 +416,13 @@ exports[`renders rejected correctly 1`] = `
itemGutter={true}
>
<RejectButton
className=""
disabled={true}
invert={true}
onClick={[Function]}
/>
<ApproveButton
className=""
invert={false}
onClick={[Function]}
/>
@@ -416,12 +444,17 @@ exports[`renders reply correctly 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<Username
<ForwardRef(forwardRef)
alignItems="center"
>
<ForwardRef(forwardRef)
className="ModerateCard-username"
onClick={[Function]}
>
Theon
</Username>
<Username>
Theon
</Username>
</ForwardRef(forwardRef)>
<Timestamp>
2018-11-29T16:01:51.897Z
</Timestamp>
@@ -429,7 +462,7 @@ exports[`renders reply correctly 1`] = `
featured={false}
onClick={[Function]}
/>
</div>
</ForwardRef(forwardRef)>
<div>
<InReplyTo>
Julian
@@ -475,7 +508,7 @@ exports[`renders reply correctly 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<ForwardRef(forwardRef)
alignItems="center"
@@ -496,10 +529,12 @@ exports[`renders reply correctly 1`] = `
itemGutter={true}
>
<RejectButton
className=""
invert={false}
onClick={[Function]}
/>
<ApproveButton
className=""
invert={false}
onClick={[Function]}
/>
@@ -521,12 +556,17 @@ exports[`renders story info 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<Username
<ForwardRef(forwardRef)
alignItems="center"
>
<ForwardRef(forwardRef)
className="ModerateCard-username"
onClick={[Function]}
>
Theon
</Username>
<Username>
Theon
</Username>
</ForwardRef(forwardRef)>
<Timestamp>
2018-11-29T16:01:51.897Z
</Timestamp>
@@ -534,7 +574,7 @@ exports[`renders story info 1`] = `
featured={false}
onClick={[Function]}
/>
</div>
</ForwardRef(forwardRef)>
</div>
<CommentContent
bannedWords={
@@ -604,7 +644,7 @@ exports[`renders story info 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<ForwardRef(forwardRef)
alignItems="center"
@@ -625,10 +665,12 @@ exports[`renders story info 1`] = `
itemGutter={true}
>
<RejectButton
className=""
invert={false}
onClick={[Function]}
/>
<ApproveButton
className=""
invert={false}
onClick={[Function]}
/>
@@ -1,5 +1,5 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import React, { FunctionComponent, useCallback, useState } from "react";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import AutoLoadMore from "coral-admin/components/AutoLoadMore";
@@ -7,6 +7,7 @@ import { Button, Flex, HorizontalGutter } from "coral-ui/components";
import { PropTypesOf } from "coral-ui/types";
import ModerateCardContainer from "../ModerateCard";
import UserHistoryDrawerContainer from "../UserHistoryDrawer/UserHistoryDrawerContainer";
import styles from "./Queue.css";
@@ -38,53 +39,76 @@ const Queue: FunctionComponent<Props> = ({
viewer,
viewNewCount,
onViewNew,
}) => (
<HorizontalGutter className={styles.root} size="double">
{Boolean(viewNewCount && viewNewCount > 0) && (
<Flex justifyContent="center" className={styles.viewNewButtonContainer}>
<Localized id="moderate-queue-viewNew" $count={viewNewCount}>
<Button
color="primary"
variant="filled"
onClick={onViewNew}
className={styles.viewNewButton}
}) => {
const [userDrawerVisible, setUserDrawerVisible] = useState(false);
const [userDrawerId, setUserDrawerID] = useState("");
const onShowUserDrawer = useCallback(
(userID: string) => {
setUserDrawerID(userID);
setUserDrawerVisible(true);
},
[setUserDrawerVisible, setUserDrawerID]
);
const onHideUserDrawer = useCallback(() => {
setUserDrawerVisible(false);
setUserDrawerID("");
}, [setUserDrawerVisible, setUserDrawerID]);
return (
<HorizontalGutter className={styles.root} size="double">
{Boolean(viewNewCount && viewNewCount > 0) && (
<Flex justifyContent="center" className={styles.viewNewButtonContainer}>
<Localized id="moderate-queue-viewNew" $count={viewNewCount}>
<Button
color="primary"
variant="filled"
onClick={onViewNew}
className={styles.viewNewButton}
>
View {viewNewCount} new comments
</Button>
</Localized>
</Flex>
)}
<TransitionGroup component={null} appear={false} enter={false} exit>
{comments.map(c => (
<CSSTransition
key={c.id}
timeout={400}
classNames={{
exit: styles.exitTransition,
exitActive: styles.exitTransitionActive,
exitDone: styles.exitTransitionDone,
}}
>
View {viewNewCount} new comments
</Button>
</Localized>
</Flex>
)}
<TransitionGroup component={null} appear={false} enter={false} exit>
{comments.map(c => (
<CSSTransition
key={c.id}
timeout={400}
classNames={{
exit: styles.exitTransition,
exitActive: styles.exitTransitionActive,
exitDone: styles.exitTransitionDone,
}}
>
<ModerateCardContainer
settings={settings}
viewer={viewer}
comment={c}
danglingLogic={danglingLogic}
showStoryInfo={Boolean(allStories)}
<ModerateCardContainer
settings={settings}
viewer={viewer}
comment={c}
danglingLogic={danglingLogic}
showStoryInfo={Boolean(allStories)}
onUsernameClicked={onShowUserDrawer}
/>
</CSSTransition>
))}
</TransitionGroup>
{hasMore && (
<Flex justifyContent="center">
<AutoLoadMore
disableLoadMore={disableLoadMore}
onLoadMore={onLoadMore}
/>
</CSSTransition>
))}
</TransitionGroup>
{hasMore && (
<Flex justifyContent="center">
<AutoLoadMore
disableLoadMore={disableLoadMore}
onLoadMore={onLoadMore}
/>
</Flex>
)}
{comments.length === 0 && emptyElement}
</HorizontalGutter>
);
</Flex>
)}
{comments.length === 0 && emptyElement}
<UserHistoryDrawerContainer
open={userDrawerVisible}
onClose={onHideUserDrawer}
userID={userDrawerId}
/>
</HorizontalGutter>
);
};
export default Queue;
@@ -19,6 +19,11 @@ exports[`renders correctly with load more 1`] = `
onLoadMore={[Function]}
/>
</ForwardRef(forwardRef)>
<UserHistoryDrawerContainer
onClose={[Function]}
open={false}
userID=""
/>
</ForwardRef(forwardRef)>
`;
@@ -33,5 +38,10 @@ exports[`renders correctly without load more 1`] = `
enter={false}
exit={true}
/>
<UserHistoryDrawerContainer
onClose={[Function]}
open={false}
userID=""
/>
</ForwardRef(forwardRef)>
`;
@@ -0,0 +1,15 @@
.footer {
text-align: center;
}
.loadMore {
width: 100%;
background-color: var(--palette-common-white);
border-width: 1px;
border-style: solid;
border-color: var(--palette-grey-lighter);
border-radius: 2px;
margin-top: var(--spacing-1);
}
@@ -0,0 +1,159 @@
import {
useLoadMore,
withPaginationContainer,
} from "coral-framework/lib/relay";
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent, useCallback } from "react";
import { graphql, RelayPaginationProp } from "react-relay";
import { Button, CallOut, Typography } from "coral-ui/components/";
import { UserHistoryAllComments_settings } from "coral-admin/__generated__/UserHistoryAllComments_settings.graphql";
import { UserHistoryAllComments_user } from "coral-admin/__generated__/UserHistoryAllComments_user.graphql";
import { UserHistoryAllComments_viewer } from "coral-admin/__generated__/UserHistoryAllComments_viewer.graphql";
import { UserHistoryAllCommentsPaginationQueryVariables } from "coral-admin/__generated__/UserHistoryAllCommentsPaginationQuery.graphql";
import { ModerateCardContainer } from "../ModerateCard";
import styles from "./UserHistoryAllComments.css";
interface Props {
user: UserHistoryAllComments_user;
viewer: UserHistoryAllComments_viewer;
settings: UserHistoryAllComments_settings;
relay: RelayPaginationProp;
}
const UserHistoryAllComments: FunctionComponent<Props> = ({
user,
viewer,
settings,
relay,
}) => {
const [loadMore, isLoadingMore] = useLoadMore(relay, 5);
const onLoadMore = useCallback(() => {
if (!loadMore || isLoadingMore) {
return;
}
loadMore();
}, [loadMore]);
const hasMore = relay.hasMore();
const comments = user ? user.allComments.edges.map(edge => edge.node) : [];
if (comments.length === 0) {
return (
<CallOut fullWidth>
<Localized
id="moderate-user-drawer-rejected-no-comments"
$username={user.username}
>
<Typography variant="bodyCopy">
{user.username} has not submitted any comments.
</Typography>
</Localized>
</CallOut>
);
}
return (
<>
{comments.map((c, index) => (
<div key={c.id}>
<ModerateCardContainer
comment={c}
viewer={viewer}
settings={settings}
danglingLogic={status => false}
hideUsername
showStoryInfo
mini
/>
{// Don't show horizontal rule after last comment
index !== comments.length - 1 && <hr />}
</div>
))}
{hasMore && (
<div className={styles.footer}>
<Button className={styles.loadMore} onClick={onLoadMore}>
<Localized id="moderate-user-drawer-load-more">Load More</Localized>
</Button>
</div>
)}
</>
);
};
type FragmentVariables = UserHistoryAllCommentsPaginationQueryVariables;
const enhanced = withPaginationContainer<
Props,
UserHistoryAllCommentsPaginationQueryVariables,
FragmentVariables
>(
{
viewer: graphql`
fragment UserHistoryAllComments_viewer on User {
...ModerateCardContainer_viewer
}
`,
settings: graphql`
fragment UserHistoryAllComments_settings on Settings {
...ModerateCardContainer_settings
}
`,
user: graphql`
fragment UserHistoryAllComments_user on User
@argumentDefinitions(
count: { type: "Int!", defaultValue: 5 }
cursor: { type: "Cursor" }
) {
username
allComments(first: $count, after: $cursor)
@connection(key: "UserHistoryAll_allComments") {
edges {
node {
id
...ModerateCardContainer_comment
}
}
}
}
`,
},
{
direction: "forward",
getConnectionFromProps(props) {
return props.user && props.user.allComments;
},
getFragmentVariables(prevVars, totalCount) {
return {
...prevVars,
count: totalCount,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
return {
count,
cursor,
userID: fragmentVariables.userID,
};
},
query: graphql`
query UserHistoryAllCommentsPaginationQuery(
$userID: ID!
$count: Int!
$cursor: Cursor
) {
user(id: $userID) {
...UserHistoryAllComments_user
@arguments(count: $count, cursor: $cursor)
}
}
`,
}
)(UserHistoryAllComments);
export default enhanced;
@@ -0,0 +1,10 @@
.root {
text-align: center;
}
.callout {
width: 100%;
font-family: var(--font-family-sans-serif);
align-content: center;
text-align: center;
}
@@ -0,0 +1,72 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { ReadyState } from "react-relay";
import { CallOut, Spinner } from "coral-ui/components";
import { UserHistoryAllCommentsContainerQuery as QueryTypes } from "coral-admin/__generated__/UserHistoryAllCommentsContainerQuery.graphql";
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
import UserHistoryAllComments from "./UserHistoryAllComments";
import styles from "./UserHistoryAllCommentsContainer.css";
interface Props {
userID: string;
}
const UserHistoryAllCommentsContainer: FunctionComponent<Props> = ({
userID,
}) => {
return (
<QueryRenderer<QueryTypes>
query={graphql`
query UserHistoryAllCommentsContainerQuery($userID: ID!) {
user(id: $userID) {
...UserHistoryAllComments_user
}
viewer {
...UserHistoryAllComments_viewer
}
settings {
...UserHistoryAllComments_settings
}
}
`}
variables={{ userID }}
cacheConfig={{ force: true }}
render={({ error, props }: ReadyState<QueryTypes["response"]>) => {
if (!props) {
return (
<div className={styles.root}>
<Spinner />
</div>
);
}
if (!props.user) {
return (
<div className={styles.callout}>
<CallOut>
<Localized id="moderate-user-drawer-user-not-found ">
User not found.
</Localized>
</CallOut>
</div>
);
}
return (
<UserHistoryAllComments
// We can never get to this part of the UI without being logged in.
viewer={props.viewer!}
settings={props.settings!}
user={props.user}
/>
);
}}
/>
);
};
export default UserHistoryAllCommentsContainer;
@@ -0,0 +1,14 @@
.root {
position: fixed;
top:0;
right: 0;
width: 624px;
height: 100%;
display: flex;
flex-flow: column nowrap;
flex-direction: column;
background-color: var(--palette-common-white);
}
@@ -0,0 +1,35 @@
import React, { FunctionComponent } from "react";
import { Card, Modal } from "coral-ui/components";
import styles from "./UserHistoryDrawerContainer.css";
import UserHistoryDrawerQuery from "./UserHistoryDrawerQuery";
interface UserHistoryDrawerContainerProps {
open: boolean;
onClose: () => void;
userID?: string;
}
const UserHistoryDrawerContainer: FunctionComponent<
UserHistoryDrawerContainerProps
> = ({ open, onClose, userID }) => {
return (
<Modal open={open} onClose={onClose}>
{({ firstFocusableRef, lastFocusableRef }) => (
<Card className={styles.root}>
{userID && (
<UserHistoryDrawerQuery
userID={userID}
onClose={onClose}
firstFocusableRef={firstFocusableRef}
lastFocusableRef={lastFocusableRef}
/>
)}
</Card>
)}
</Modal>
);
};
export default UserHistoryDrawerContainer;
@@ -0,0 +1,94 @@
.root {
position: fixed;
top:0;
right: 0;
width: 624px;
height: 100%;
display: flex;
flex-flow: column nowrap;
flex-direction: column;
background-color: var(--palette-common-white);
}
.comments {
flex: auto;
overflow: hidden;
}
.close {
position: absolute;
width: 40px;
height: 40px;
left: -40px;
padding: 0px;
background-color: var(--palette-common-white);
border-width: 2px;
border-right-width: 0px;
border-style: solid;
border-color: var(--palette-grey-main);
border-radius: 4px 0px 0px 4px;
}
.username {
font-family: var(--font-family-serif);
font-size: calc(24rem / var(--rem-base));
font-weight: 500;
font-family: var(--font-family-serif);
line-height: calc(36em / 24);
letter-spacing: calc(0.2em / 24);
margin-bottom: var(--spacing-1);
}
.userDetails {
margin-bottom: var(--spacing-3);
}
.userDetail {
margin-bottom: var(--spacing-1);
}
.userDetailValue {
margin-right: var(--spacing-2);
}
.icon {
margin-right: var(--spacing-2);
}
.copy {
border: 1px solid var(--palette-primary-main);
background-color: transparent;
color: var(--palette-primary-main);
padding-top: 2px;
padding-bottom: 2px;
&:active {
background-color: var(--palette-primary-lightest);
border-color: var(--palette-primary-main);
color: var(--palette-primary-main);
}
}
.callout {
width: 100%;
font-family: var(--font-family-sans-serif);
align-content: center;
text-align: center;
}
hr {
border: 0;
border-bottom: 1px solid var(--palette-divider);
padding-top: 1px;
width: 100%;
}
.divider {
border-bottom: 1px solid var(--palette-grey-lighter);
}
@@ -0,0 +1,137 @@
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { ReadyState } from "react-relay";
import { CopyButton } from "coral-framework/components";
import {
Button,
CallOut,
Flex,
Icon,
Spinner,
Typography,
} from "coral-ui/components";
import { UserHistoryDrawerQuery as QueryTypes } from "coral-admin/__generated__/UserHistoryDrawerQuery.graphql";
import UserHistoryTabs from "./UserHistoryTabs";
import styles from "./UserHistoryDrawerQuery.css";
interface Props {
userID: string;
onClose: () => void;
firstFocusableRef: React.RefObject<any>;
lastFocusableRef: React.RefObject<any>;
}
const UserHistoryDrawerQuery: FunctionComponent<Props> = ({
userID,
onClose,
}) => {
return (
<QueryRenderer<QueryTypes>
query={graphql`
query UserHistoryDrawerQuery($userID: ID!) {
user(id: $userID) {
id
username
email
createdAt
}
}
`}
variables={{ userID }}
cacheConfig={{ force: true }}
render={({ error, props }: ReadyState<QueryTypes["response"]>) => {
if (!props) {
return (
<div className={styles.root}>
<Spinner />
</div>
);
}
if (!props.user) {
return (
<div className={styles.callout}>
<CallOut>
<Localized id="moderate-user-drawer-user-not-found ">
User not found.
</Localized>
</CallOut>
</div>
);
}
const user = props.user;
return (
<>
<Button className={styles.close} onClick={onClose}>
<Icon size="md">close</Icon>
</Button>
<Flex className={styles.username}>
<span>{user.username}</span>
</Flex>
<div className={styles.userDetails}>
<Flex alignItems="center" className={styles.userDetail}>
<Icon size="sm" className={styles.icon}>
mail_outline
</Icon>
<Typography
variant="bodyCopy"
container="span"
className={styles.userDetailValue}
>
{user.email}
</Typography>
<CopyButton
text={user.email!}
variant="regular"
className={styles.copy}
/>
</Flex>
<Flex alignItems="center" className={styles.userDetail}>
<Icon size="sm" className={styles.icon}>
date_range
</Icon>
<Typography variant="bodyCopy" container="span">
{new Date(user.createdAt).toLocaleDateString("en-us", {
month: "long",
day: "numeric",
year: "numeric",
})}
</Typography>
</Flex>
<Flex alignItems="center" className={styles.userDetail}>
<Icon size="sm" className={styles.icon}>
people_outline
</Icon>
<Typography
variant="bodyCopy"
container="span"
className={styles.userDetailValue}
>
{user.id}
</Typography>
<CopyButton
text={user.id}
variant="regular"
className={styles.copy}
/>
</Flex>
</div>
<hr className={styles.divider} />
<div className={styles.comments}>
<UserHistoryTabs userID={user.id} />
</div>
</>
);
}}
/>
);
};
export default UserHistoryDrawerQuery;
@@ -0,0 +1,15 @@
.footer {
text-align: center;
}
.loadMore {
width: 100%;
background-color: var(--palette-common-white);
border-width: 1px;
border-style: solid;
border-color: var(--palette-grey-lighter);
border-radius: 2px;
margin-top: var(--spacing-1);
}
@@ -0,0 +1,161 @@
import {
useLoadMore,
withPaginationContainer,
} from "coral-framework/lib/relay";
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent, useCallback } from "react";
import { graphql, RelayPaginationProp } from "react-relay";
import { Button, CallOut, Typography } from "coral-ui/components";
import { UserHistoryRejectedComments_settings } from "coral-admin/__generated__/UserHistoryRejectedComments_settings.graphql";
import { UserHistoryRejectedComments_user } from "coral-admin/__generated__/UserHistoryRejectedComments_user.graphql";
import { UserHistoryRejectedComments_viewer } from "coral-admin/__generated__/UserHistoryRejectedComments_viewer.graphql";
import { UserHistoryRejectedCommentsPaginationQueryVariables } from "coral-admin/__generated__/UserHistoryRejectedCommentsPaginationQuery.graphql";
import { ModerateCardContainer } from "../ModerateCard";
import styles from "./UserHistoryRejectedComments.css";
interface Props {
user: UserHistoryRejectedComments_user;
viewer: UserHistoryRejectedComments_viewer;
settings: UserHistoryRejectedComments_settings;
relay: RelayPaginationProp;
}
const UserHistoryRejectedComments: FunctionComponent<Props> = ({
user,
viewer,
settings,
relay,
}) => {
const [loadMore, isLoadingMore] = useLoadMore(relay, 5);
const onLoadMore = useCallback(() => {
if (!loadMore || isLoadingMore) {
return;
}
loadMore();
}, [loadMore]);
const hasMore = relay.hasMore();
const comments = user
? user.rejectedComments.edges.map(edge => edge.node)
: [];
if (comments.length === 0) {
return (
<CallOut fullWidth>
<Localized
id="moderate-user-drawer-rejected-no-comments"
$username={user.username}
>
<Typography variant="bodyCopy">
{user.username} does not have any rejected comments.
</Typography>
</Localized>
</CallOut>
);
}
return (
<>
{comments.map((c, index) => (
<div key={c.id}>
<ModerateCardContainer
comment={c}
viewer={viewer}
settings={settings}
danglingLogic={status => false}
hideUsername
showStoryInfo
mini
/>
{// Don't show horizontal rule after last comment
index !== comments.length - 1 && <hr />}
</div>
))}
{hasMore && (
<div className={styles.footer}>
<Button className={styles.loadMore} onClick={onLoadMore}>
<Localized id="moderate-user-drawer-load-more">Load More</Localized>
</Button>
</div>
)}
</>
);
};
type FragmentVariables = UserHistoryRejectedCommentsPaginationQueryVariables;
const enhanced = withPaginationContainer<
Props,
UserHistoryRejectedCommentsPaginationQueryVariables,
FragmentVariables
>(
{
viewer: graphql`
fragment UserHistoryRejectedComments_viewer on User {
...ModerateCardContainer_viewer
}
`,
settings: graphql`
fragment UserHistoryRejectedComments_settings on Settings {
...ModerateCardContainer_settings
}
`,
user: graphql`
fragment UserHistoryRejectedComments_user on User
@argumentDefinitions(
count: { type: "Int!", defaultValue: 5 }
cursor: { type: "Cursor" }
) {
username
rejectedComments(first: $count, after: $cursor)
@connection(key: "UserHistoryRejected_rejectedComments") {
edges {
node {
id
...ModerateCardContainer_comment
}
}
}
}
`,
},
{
direction: "forward",
getConnectionFromProps(props) {
return props.user && props.user.rejectedComments;
},
getFragmentVariables(prevVars, totalCount) {
return {
...prevVars,
count: totalCount,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
return {
count,
cursor,
userID: fragmentVariables.userID,
};
},
query: graphql`
query UserHistoryRejectedCommentsPaginationQuery(
$userID: ID!
$count: Int!
$cursor: Cursor
) {
user(id: $userID) {
...UserHistoryRejectedComments_user
@arguments(count: $count, cursor: $cursor)
}
}
`,
}
)(UserHistoryRejectedComments);
export default enhanced;
@@ -0,0 +1,10 @@
.root {
text-align: center;
}
.callout {
width: 100%;
font-family: var(--font-family-sans-serif);
align-content: center;
text-align: center;
}
@@ -0,0 +1,72 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { ReadyState } from "react-relay";
import { CallOut, Spinner } from "coral-ui/components";
import { UserHistoryRejectedCommentsContainerQuery as QueryTypes } from "coral-admin/__generated__/UserHistoryRejectedCommentsContainerQuery.graphql";
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
import UserHistoryRejectedComments from "./UserHistoryRejectedComments";
import styles from "./UserHistoryRejectedCommentsContainer.css";
interface Props {
userID: string;
}
const UserHistoryRejectedCommentsContainer: FunctionComponent<Props> = ({
userID,
}) => {
return (
<QueryRenderer<any>
query={graphql`
query UserHistoryRejectedCommentsContainerQuery($userID: ID!) {
user(id: $userID) {
...UserHistoryRejectedComments_user
}
viewer {
...UserHistoryRejectedComments_viewer
}
settings {
...UserHistoryRejectedComments_settings
}
}
`}
variables={{ userID }}
cacheConfig={{ force: true }}
render={({ error, props }: ReadyState<QueryTypes["response"]>) => {
if (!props) {
return (
<div className={styles.root}>
<Spinner />
</div>
);
}
if (!props.user) {
return (
<div className={styles.callout}>
<CallOut>
<Localized id="moderate-user-drawer-user-not-found ">
User not found.
</Localized>
</CallOut>
</div>
);
}
return (
<UserHistoryRejectedComments
// We can never get to this part of the UI without being logged in.
viewer={props.viewer!}
settings={props.settings!}
user={props.user}
/>
);
}}
/>
);
};
export default UserHistoryRejectedCommentsContainer;
@@ -0,0 +1,59 @@
.root {
height: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
min-height: 0;
overflow: hidden;
}
.tabBar {
flex: none;
border-bottom: 0px;
}
.tab {
color: var(--palette-grey-dark);
height: 100%;
display: inline-flex;
align-items: center;
text-transform: uppercase;
text-decoration: none;
box-sizing: border-box;
border-bottom: 3px solid transparent;
font-size: calc(14rem / var(--rem-base));
}
.activeTab {
color: var(--palette-primary-main);
}
.tabIcon {
margin-right: var(--spacing-2);
}
.tabContent {
flex: auto;
overflow: hidden;
}
.container {
height: calc(100% - 2 * var(--spacing-2));
display: flex;
flex-direction: column;
align-items: stretch;
min-height: 0;
overflow: hidden;
margin-bottom: var(--spacing-2);
margin-top: var(--spacing-2);
}
.scrollable {
overflow: auto;
}
@@ -0,0 +1,85 @@
import cn from "classnames";
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent, useCallback, useState } from "react";
import { Icon, Tab, TabBar, TabContent, TabPane } from "coral-ui/components";
import UserHistoryAllCommentsContainer from "./UserHistoryAllCommentsContainer";
import UserHistoryRejectedCommentsContainer from "./UserHistoryRejectedCommentsContainer";
import styles from "./UserHistoryTabs.css";
type TabType = "ALL" | "REJECTED";
interface Props {
userID: string;
}
const UserHistoryTabs: FunctionComponent<Props> = ({ userID }) => {
const [currentTab, setCurrentTab] = useState<TabType>("ALL");
const onTabChanged = useCallback(
(tab: TabType) => {
setCurrentTab(tab);
},
[setCurrentTab]
);
return (
<div className={styles.root}>
<TabBar
activeTab={currentTab}
variant="secondary"
onTabClick={onTabChanged}
className={styles.tabBar}
>
<Tab tabID="ALL" onTabClick={onTabChanged}>
<div
className={cn(styles.tab, {
[styles.activeTab]: currentTab === "ALL",
})}
>
<Icon size="sm" className={styles.tabIcon}>
forum
</Icon>
<Localized id="moderate-user-drawer-tab-all-comments">
<span>All Comments</span>
</Localized>
</div>
</Tab>
<Tab tabID="REJECTED" onTabClick={onTabChanged}>
<div
className={cn(styles.tab, {
[styles.activeTab]: currentTab === "REJECTED",
})}
>
<Icon size="sm" className={styles.tabIcon}>
cancel
</Icon>
<Localized id="moderate-user-drawer-tab-rejected-comments">
<span>Rejected</span>
</Localized>
</div>
</Tab>
</TabBar>
<TabContent activeTab={currentTab} className={styles.tabContent}>
<TabPane tabID="ALL">
<div className={styles.container}>
<div className={styles.scrollable}>
<UserHistoryAllCommentsContainer userID={userID} />
</div>
</div>
</TabPane>
<TabPane tabID="REJECTED">
<div className={styles.container}>
<div className={styles.scrollable}>
<UserHistoryRejectedCommentsContainer userID={userID} />
</div>
</div>
</TabPane>
</TabContent>
</div>
);
};
export default UserHistoryTabs;
@@ -27,12 +27,25 @@ exports[`approves comment in reported queue: dangling 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Isabelle
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Isabelle
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -159,7 +172,7 @@ exports[`approves comment in reported queue: dangling 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -243,12 +256,25 @@ exports[`rejects comment in reported queue: dangling 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Isabelle
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Isabelle
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -375,7 +401,7 @@ exports[`rejects comment in reported queue: dangling 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -456,12 +482,25 @@ exports[`renders reported queue with comments 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Isabelle
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Isabelle
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -588,7 +627,7 @@ exports[`renders reported queue with comments 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -656,12 +695,25 @@ exports[`renders reported queue with comments 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Ngoc
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Ngoc
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -788,7 +840,7 @@ exports[`renders reported queue with comments 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -872,12 +924,25 @@ exports[`renders reported queue with comments 2`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Isabelle
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Isabelle
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -1004,7 +1069,7 @@ exports[`renders reported queue with comments 2`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -1072,12 +1137,25 @@ exports[`renders reported queue with comments 2`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Ngoc
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Ngoc
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -1204,7 +1282,7 @@ exports[`renders reported queue with comments 2`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -1278,12 +1356,25 @@ exports[`renders reported queue with comments and load more 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Max
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Max
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -1428,7 +1519,7 @@ exports[`renders reported queue with comments and load more 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -27,12 +27,25 @@ exports[`approves comment in rejected queue: dangling 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Isabelle
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Isabelle
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -159,7 +172,7 @@ exports[`approves comment in rejected queue: dangling 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -240,12 +253,25 @@ exports[`renders rejected queue with comments 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Isabelle
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Isabelle
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -372,7 +398,7 @@ exports[`renders rejected queue with comments 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -440,12 +466,25 @@ exports[`renders rejected queue with comments 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Ngoc
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Ngoc
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -572,7 +611,7 @@ exports[`renders rejected queue with comments 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -646,12 +685,25 @@ exports[`renders rejected queue with comments and load more 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Max
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Max
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -791,7 +843,7 @@ exports[`renders rejected queue with comments and load more 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -14,12 +14,25 @@ exports[`approves single comment 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Isabelle
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Isabelle
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -125,7 +138,7 @@ exports[`approves single comment 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -196,12 +209,25 @@ exports[`rejects single comment 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Isabelle
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Isabelle
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -307,7 +333,7 @@ exports[`rejects single comment 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -413,12 +439,25 @@ exports[`renders single comment view 1`] = `
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
<div
className="Box-root Flex-root Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root ModerateCard-username"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Isabelle
</span>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary Username-root"
>
Isabelle
</span>
</button>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
@@ -524,7 +563,7 @@ exports[`renders single comment view 1`] = `
</div>
</div>
<div
className="ModerateCard-separator"
className="ModerateCard-separator ModerateCard-ruledSeparator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
@@ -11,17 +11,22 @@ import {
GQLUSER_ROLE,
QueryToCommentsArgs,
StoryToCommentsArgs,
UserToAllCommentsArgs,
UserToCommentsArgs,
UserToRejectedCommentsArgs,
} from "coral-server/graph/tenant/schema/__generated__/types";
import { retrieveManyUserActionPresence } from "coral-server/models/action/comment";
import {
Comment,
CommentConnectionInput,
retrieveAllCommentsUserConnection,
retrieveCommentConnection,
retrieveCommentParentsConnection,
retrieveCommentRepliesConnection,
retrieveCommentStoryConnection,
retrieveCommentUserConnection,
retrieveManyComments,
retrieveRejectedCommentUserConnection,
retrieveStoryCommentTagCounts,
} from "coral-server/models/comment";
import { hasVisibleStatus } from "coral-server/models/comment/helpers";
@@ -153,13 +158,33 @@ export default (ctx: Context) => ({
first = 10,
orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC,
after,
}: StoryToCommentsArgs
}: UserToCommentsArgs
) =>
retrieveCommentUserConnection(ctx.mongo, ctx.tenant.id, userID, {
first,
orderBy,
after,
}).then(primeCommentsFromConnection(ctx)),
forUserAll: (
userID: string,
// Apply the graph schema defaults at the loader.
{ first = 10, after }: UserToAllCommentsArgs
) =>
retrieveAllCommentsUserConnection(ctx.mongo, ctx.tenant.id, userID, {
first,
orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC,
after,
}).then(primeCommentsFromConnection(ctx)),
forUserRejected: (
userID: string,
// Apply the graph schema defaults at the loader.
{ first = 10, after }: UserToRejectedCommentsArgs
) =>
retrieveRejectedCommentUserConnection(ctx.mongo, ctx.tenant.id, userID, {
first,
orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC,
after,
}).then(primeCommentsFromConnection(ctx)),
taggedForStory: (
storyID: string,
tag: GQLTAG,
@@ -34,6 +34,10 @@ const maybeLoadOnlyIgnoredUserID = (
export const User: GQLUserTypeResolver<user.User> = {
comments: ({ id }, input, ctx) => ctx.loaders.Comments.forUser(id, input),
allComments: ({ id }, input, ctx) =>
ctx.loaders.Comments.forUserAll(id, input),
rejectedComments: ({ id }, input, ctx) =>
ctx.loaders.Comments.forUserRejected(id, input),
commentModerationActionHistory: ({ id }, input, ctx) =>
ctx.loaders.CommentModerationActions.forModerator(input, id),
status: ({ id, status }): UserStatusInput => ({
@@ -1521,6 +1521,18 @@ type User {
permit: [SUSPENDED, BANNED]
)
"""
allComments are comments regardless of visibility status.
"""
allComments(first: Int = 10, after: Cursor): CommentsConnection!
@auth(roles: [ADMIN, MODERATOR])
"""
rejectedComments are comments that have been rejected.
"""
rejectedComments(first: Int = 10, after: Cursor): CommentsConnection!
@auth(roles: [ADMIN, MODERATOR])
"""
commentModerationActionHistory returns a CommentModerationActionConnection
that this User has created.
+51
View File
@@ -657,6 +657,57 @@ export const retrieveCommentUserConnection = (
},
});
/**
* retrieveAllCommentUserConnection returns a Connection<Comment> for a given User's
* comments regardless of comment status.
*
* @param mongo database connection
* @param tenantID the Tenant's ID
* @param userID the User id for the comment to retrieve
* @param input connection configuration
*/
export const retrieveAllCommentsUserConnection = (
mongo: Db,
tenantID: string,
userID: string,
input: CommentConnectionInput
) =>
retrieveCommentConnection(mongo, tenantID, {
...input,
filter: {
...input.filter,
authorID: userID,
},
});
/**
* retrieveRejectedCommentUserConnection returns a Connection<Comment> for a given User's
* rejected comments.
*
* @param mongo database connection
* @param tenantID the Tenant's ID
* @param userID the User id for the comment to retrieve
* @param input connection configuration
*/
export const retrieveRejectedCommentUserConnection = (
mongo: Db,
tenantID: string,
userID: string,
input: CommentConnectionInput
) =>
retrieveStatusCommentConnection(
mongo,
tenantID,
[GQLCOMMENT_STATUS.REJECTED],
{
...input,
filter: {
...input.filter,
authorID: userID,
},
}
);
/**
* retrieveVisibleCommentConnection will retrieve a connection that contains
* comments that are visible.
+9
View File
@@ -388,6 +388,15 @@ moderate-searchBar-comboBoxTextField =
moderate-searchBar-goTo = Go to
moderate-searchBar-seeAllResults = See all results
### Moderate User History Drawer
moderate-user-drawer-tab-all-comments = All Comments
moderate-user-drawer-tab-rejected-comments = Rejected
moderate-user-drawer-load-more = Load More
moderate-user-drawer-all-no-comments = {$username} has not submitted any comments.
moderate-user-drawer-rejected-no-comments = {$username} does not have any rejected comments.
moderate-user-drawer-user-not-found = User not found.
## Create Username
createUsername-createUsernameHeader = Create Username