mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 12:15:40 +08:00
Merge branch 'next-comment-counts' of github.com:coralproject/talk into next-comment-counts
* 'next-comment-counts' of github.com:coralproject/talk: (53 commits) fix: Readd and fix App unittest fix: undesired margin Correct optimitic updates fix: correct typings fix: linting Optimistic Responses updated Adding optimisticresponse for deleteCommentReaction Changes test: use baseComment and baseAsset in fixtures fix: lint test: simplify tests fix: pass settings to deeper ReplyListContainer fix: remove debug fix: test updates feat: replaced respect with reaction and added some options! Adding spected More snapshots It was the optimistic response Adding actionCounts to the fixtures Updated snapshots ...
This commit is contained in:
Generated
+6
-6
@@ -1647,9 +1647,9 @@
|
||||
}
|
||||
},
|
||||
"@types/bson": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.10.tgz",
|
||||
"integrity": "sha512-gRf+Qy5Qiyjz28ZkPRP37bDHtGG67op/lV2qcIMhWUq4vIMJ6/j13ajeYH7LFhJ5RNflyLHmdANPGDXZ5a8EzQ==",
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.11.tgz",
|
||||
"integrity": "sha512-j+UcCWI+FsbI5/FQP/Kj2CXyplWAz39ktHFkXk84h7dNblKRSoNJs95PZFRd96NQGqsPEPgeclqnznWZr14ZDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
@@ -1989,9 +1989,9 @@
|
||||
}
|
||||
},
|
||||
"@types/mongodb": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.1.tgz",
|
||||
"integrity": "sha512-lHEH+OwYNeuC28jlmdPT/wBAVMuB6M1sHjZKAtaho/LeJf78ILJPYUq2OD7mWVj9QR7uYiKVt4ExGkXegHFCJQ==",
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.8.tgz",
|
||||
"integrity": "sha512-5higsHdPx63XKIh5hjr5GGrCCErBqEbpZZiNsUcqk97mMDpCBH9R4dRi/T8bcMrQItCdL+wecagdAj3JPKkuVg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/bson": "*",
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@
|
||||
"@types/lodash": "^4.14.111",
|
||||
"@types/luxon": "^0.5.3",
|
||||
"@types/mini-css-extract-plugin": "^0.2.0",
|
||||
"@types/mongodb": "^3.1.1",
|
||||
"@types/mongodb": "^3.1.8",
|
||||
"@types/ms": "^0.7.30",
|
||||
"@types/node": "^10.5.2",
|
||||
"@types/node-fetch": "^2.1.2",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { shallow } from "enzyme";
|
||||
import noop from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
it("renders comments", () => {
|
||||
const props: PropTypesOf<typeof App> = {
|
||||
activeTab: "COMMENTS",
|
||||
onTabClick: noop,
|
||||
};
|
||||
const wrapper = shallow(<App {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders comments 1`] = `
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
className="App-root"
|
||||
>
|
||||
<withPropsOnChange(TabBar)
|
||||
activeTab="COMMENTS"
|
||||
onTabClick={[Function]}
|
||||
>
|
||||
<CommentsTab
|
||||
tabId="COMMENTS"
|
||||
/>
|
||||
<MyProfileTab
|
||||
tabId="PROFILE"
|
||||
/>
|
||||
</withPropsOnChange(TabBar)>
|
||||
<TabContent
|
||||
activeTab="COMMENTS"
|
||||
className="App-tabContent"
|
||||
>
|
||||
<TabPane
|
||||
tabId="COMMENTS"
|
||||
>
|
||||
<withContext(withLocalStateContainer(CommentsPaneContainer)) />
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tabId="PROFILE"
|
||||
>
|
||||
<withContext(withLocalStateContainer(ProfileQuery)) />
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
`;
|
||||
@@ -158,6 +158,11 @@ function commit(
|
||||
editing: {
|
||||
editableUntil: new Date(Date.now() + 10000),
|
||||
},
|
||||
actionCounts: {
|
||||
reaction: {
|
||||
total: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { Omit } from "talk-framework/types";
|
||||
|
||||
import { CreateCommentReactionMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentReactionMutation.graphql";
|
||||
|
||||
export type CreateCommentReactionInput = Omit<
|
||||
MutationTypes["variables"]["input"],
|
||||
"clientMutationId"
|
||||
>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation CreateCommentReactionMutation($input: CreateCommentReactionInput!) {
|
||||
createCommentReaction(input: $input) {
|
||||
comment {
|
||||
...ReactionButtonContainer_comment
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: CreateCommentReactionInput) {
|
||||
const source = environment.getStore().getSource();
|
||||
const currentCount = source.get(
|
||||
source.get(source.get(input.commentID)!.actionCounts.__ref)!.reaction.__ref
|
||||
)!.total;
|
||||
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
createCommentReaction: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
myActionPresence: {
|
||||
reaction: true,
|
||||
},
|
||||
actionCounts: {
|
||||
reaction: {
|
||||
total: currentCount + 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
|
||||
});
|
||||
}
|
||||
|
||||
export const withCreateCommentReactionMutation = createMutationContainer(
|
||||
"createCommentReaction",
|
||||
commit
|
||||
);
|
||||
|
||||
export type CreateCommentReactionMutation = (
|
||||
input: CreateCommentReactionInput
|
||||
) => Promise<MutationTypes["response"]["createCommentReaction"]>;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { DeleteCommentReactionMutation as MutationTypes } from "talk-stream/__generated__/DeleteCommentReactionMutation.graphql";
|
||||
import { CreateCommentReactionInput } from "./CreateCommentReactionMutation";
|
||||
|
||||
const mutation = graphql`
|
||||
mutation DeleteCommentReactionMutation($input: CreateCommentReactionInput!) {
|
||||
deleteCommentReaction(input: $input) {
|
||||
comment {
|
||||
...ReactionButtonContainer_comment
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: CreateCommentReactionInput) {
|
||||
const source = environment.getStore().getSource();
|
||||
const currentCount = source.get(
|
||||
source.get(source.get(input.commentID)!.actionCounts.__ref)!.reaction.__ref
|
||||
)!.total;
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
deleteCommentReaction: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
myActionPresence: {
|
||||
reaction: false,
|
||||
},
|
||||
actionCounts: {
|
||||
reaction: {
|
||||
total: currentCount - 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
|
||||
});
|
||||
}
|
||||
|
||||
export const withDeleteCommentReactionMutation = createMutationContainer(
|
||||
"deleteCommentReaction",
|
||||
commit
|
||||
);
|
||||
|
||||
export type DeleteCommentReactionMutation = (
|
||||
input: CreateCommentReactionInput
|
||||
) => Promise<MutationTypes["response"]["deleteCommentReaction"]>;
|
||||
@@ -31,3 +31,12 @@ export {
|
||||
withSetActiveTabMutation,
|
||||
SetActiveTabMutation,
|
||||
} from "./SetActiveTabMutation";
|
||||
export {
|
||||
withCreateCommentReactionMutation,
|
||||
CreateCommentReactionMutation,
|
||||
CreateCommentReactionInput,
|
||||
} from "./CreateCommentReactionMutation";
|
||||
export {
|
||||
withDeleteCommentReactionMutation,
|
||||
DeleteCommentReactionMutation,
|
||||
} from "./DeleteCommentReactionMutation";
|
||||
|
||||
@@ -35,7 +35,7 @@ class Permalink extends React.Component<PermalinkProps> {
|
||||
id={popoverID}
|
||||
placement="top-start"
|
||||
description="A dialog showing a permalink to the comment"
|
||||
className={styles.popover}
|
||||
classes={{ popover: styles.popover }}
|
||||
body={({ toggleVisibility }) => (
|
||||
<ClickOutside
|
||||
onClickOutside={() =>
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as styles from "./PermalinkView.css";
|
||||
export interface PermalinkViewProps {
|
||||
me: PropTypesOf<typeof CommentContainer>["me"];
|
||||
asset: PropTypesOf<typeof CommentContainer>["asset"];
|
||||
settings: PropTypesOf<typeof CommentContainer>["settings"];
|
||||
comment: PropTypesOf<typeof CommentContainer>["comment"] | null;
|
||||
showAllCommentsHref: string | null;
|
||||
onShowAllComments: (e: MouseEvent<any>) => void;
|
||||
@@ -18,6 +19,7 @@ export interface PermalinkViewProps {
|
||||
const PermalinkView: StatelessComponent<PermalinkViewProps> = ({
|
||||
showAllCommentsHref,
|
||||
comment,
|
||||
settings,
|
||||
asset,
|
||||
onShowAllComments,
|
||||
me,
|
||||
@@ -46,7 +48,14 @@ const PermalinkView: StatelessComponent<PermalinkViewProps> = ({
|
||||
<Typography>Comment not found</Typography>
|
||||
</Localized>
|
||||
)}
|
||||
{comment && <CommentContainer me={me} comment={comment} asset={asset} />}
|
||||
{comment && (
|
||||
<CommentContainer
|
||||
settings={settings}
|
||||
me={me}
|
||||
comment={comment}
|
||||
asset={asset}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
|
||||
import { Button, ButtonIcon, MatchMedia } from "talk-ui/components";
|
||||
|
||||
interface ReactionButtonProps {
|
||||
onClick: () => void;
|
||||
totalReactions: number;
|
||||
reacted: boolean | null;
|
||||
label: string;
|
||||
labelActive: string | null;
|
||||
icon: string;
|
||||
iconActive: string | null;
|
||||
// color: string;
|
||||
}
|
||||
|
||||
class ReactionButton extends React.Component<ReactionButtonProps> {
|
||||
public render() {
|
||||
const { totalReactions, reacted } = this.props;
|
||||
return (
|
||||
<Button variant="ghost" size="small" onClick={this.props.onClick}>
|
||||
{!!totalReactions && <span>{totalReactions}</span>}
|
||||
<MatchMedia gtWidth="xs">
|
||||
<ButtonIcon>
|
||||
{reacted
|
||||
? this.props.iconActive
|
||||
? this.props.iconActive
|
||||
: this.props.icon
|
||||
: this.props.icon}
|
||||
</ButtonIcon>
|
||||
</MatchMedia>
|
||||
<span>
|
||||
{reacted
|
||||
? this.props.labelActive
|
||||
? this.props.labelActive
|
||||
: this.props.label
|
||||
: this.props.label}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactionButton;
|
||||
@@ -25,6 +25,12 @@ it("renders correctly", () => {
|
||||
me: null,
|
||||
localReply: false,
|
||||
disableReplies: false,
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(<ReplyListN {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -40,6 +46,12 @@ describe("when there is more", () => {
|
||||
disableShowAll: false,
|
||||
indentLevel: 1,
|
||||
me: null,
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<ReplyListN {...props} />);
|
||||
|
||||
@@ -15,29 +15,21 @@ export interface ReplyListProps {
|
||||
id: string;
|
||||
};
|
||||
comments: ReadonlyArray<
|
||||
{ id: string; showConversationLink?: boolean } & PropTypesOf<
|
||||
typeof CommentContainer
|
||||
>["comment"]
|
||||
{
|
||||
id: string;
|
||||
replyListElement?: React.ReactElement<any>;
|
||||
showConversationLink?: boolean;
|
||||
} & PropTypesOf<typeof CommentContainer>["comment"]
|
||||
>;
|
||||
settings: PropTypesOf<typeof CommentContainer>["settings"];
|
||||
onShowAll?: () => void;
|
||||
hasMore?: boolean;
|
||||
disableShowAll?: boolean;
|
||||
indentLevel?: number;
|
||||
ReplyListComponent?: React.ComponentType<any>;
|
||||
localReply?: boolean;
|
||||
disableReplies?: boolean;
|
||||
}
|
||||
|
||||
function getReplyListElement(
|
||||
{ ReplyListComponent, me, asset }: ReplyListProps,
|
||||
comment: PropTypesOf<typeof CommentContainer>["comment"]
|
||||
) {
|
||||
if (!ReplyListComponent) {
|
||||
return null;
|
||||
}
|
||||
return <ReplyListComponent me={me} comment={comment} asset={asset} />;
|
||||
}
|
||||
|
||||
const ReplyList: StatelessComponent<ReplyListProps> = props => {
|
||||
return (
|
||||
<HorizontalGutter
|
||||
@@ -51,12 +43,13 @@ const ReplyList: StatelessComponent<ReplyListProps> = props => {
|
||||
me={props.me}
|
||||
comment={comment}
|
||||
asset={props.asset}
|
||||
settings={props.settings}
|
||||
indentLevel={props.indentLevel}
|
||||
localReply={props.localReply}
|
||||
disableReplies={props.disableReplies}
|
||||
showConversationLink={!!comment.showConversationLink}
|
||||
/>
|
||||
{getReplyListElement(props, comment)}
|
||||
{comment.replyListElement}
|
||||
</HorizontalGutter>
|
||||
))}
|
||||
{props.hasMore && (
|
||||
|
||||
@@ -17,6 +17,12 @@ it("renders correctly", () => {
|
||||
isClosed: false,
|
||||
},
|
||||
comments: [{ id: "comment-1" }, { id: "comment-2" }],
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
onLoadMore: noop,
|
||||
disableLoadMore: false,
|
||||
hasMore: false,
|
||||
@@ -38,6 +44,12 @@ describe("when use is logged in", () => {
|
||||
disableLoadMore: false,
|
||||
hasMore: false,
|
||||
me: {},
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(<StreamN {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -51,6 +63,12 @@ describe("when there is more", () => {
|
||||
isClosed: false,
|
||||
},
|
||||
comments: [{ id: "comment-1" }, { id: "comment-2" }],
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
onLoadMore: sinon.spy(),
|
||||
disableLoadMore: false,
|
||||
hasMore: true,
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface StreamProps {
|
||||
isClosed?: boolean;
|
||||
} & PropTypesOf<typeof CommentContainer>["asset"] &
|
||||
PropTypesOf<typeof ReplyListContainer>["asset"];
|
||||
settings: PropTypesOf<typeof CommentContainer>["settings"] &
|
||||
PropTypesOf<typeof ReplyListContainer>["settings"];
|
||||
comments: ReadonlyArray<
|
||||
{ id: string } & PropTypesOf<typeof CommentContainer>["comment"] &
|
||||
PropTypesOf<typeof ReplyListContainer>["comment"]
|
||||
@@ -52,10 +54,12 @@ const Stream: StatelessComponent<StreamProps> = props => {
|
||||
<HorizontalGutter key={comment.id}>
|
||||
<CommentContainer
|
||||
me={props.me}
|
||||
settings={props.settings}
|
||||
comment={comment}
|
||||
asset={props.asset}
|
||||
/>
|
||||
<ReplyListContainer
|
||||
settings={props.settings}
|
||||
me={props.me}
|
||||
comment={comment}
|
||||
asset={props.asset}
|
||||
|
||||
@@ -24,6 +24,14 @@ exports[`renders correctly 1`] = `
|
||||
key="comment-1"
|
||||
localReply={false}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
@@ -47,6 +55,14 @@ exports[`renders correctly 1`] = `
|
||||
key="comment-2"
|
||||
localReply={false}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
showConversationLink={true}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
@@ -75,6 +91,14 @@ exports[`when there is more disables load more button 1`] = `
|
||||
indentLevel={1}
|
||||
key="comment-1"
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
@@ -95,6 +119,14 @@ exports[`when there is more disables load more button 1`] = `
|
||||
indentLevel={1}
|
||||
key="comment-2"
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
@@ -142,6 +174,14 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
indentLevel={1}
|
||||
key="comment-1"
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
@@ -162,6 +202,14 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
indentLevel={1}
|
||||
key="comment-2"
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
|
||||
@@ -34,6 +34,14 @@ exports[`renders correctly 1`] = `
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<withProps(Relay(ReplyListContainer))
|
||||
asset={
|
||||
@@ -48,6 +56,14 @@ exports[`renders correctly 1`] = `
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
@@ -66,6 +82,14 @@ exports[`renders correctly 1`] = `
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<withProps(Relay(ReplyListContainer))
|
||||
asset={
|
||||
@@ -80,6 +104,14 @@ exports[`renders correctly 1`] = `
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
@@ -120,6 +152,14 @@ exports[`when there is more disables load more button 1`] = `
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<withProps(Relay(ReplyListContainer))
|
||||
asset={
|
||||
@@ -134,6 +174,14 @@ exports[`when there is more disables load more button 1`] = `
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
@@ -152,6 +200,14 @@ exports[`when there is more disables load more button 1`] = `
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<withProps(Relay(ReplyListContainer))
|
||||
asset={
|
||||
@@ -166,6 +222,14 @@ exports[`when there is more disables load more button 1`] = `
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
<Localized
|
||||
@@ -220,6 +284,14 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<withProps(Relay(ReplyListContainer))
|
||||
asset={
|
||||
@@ -234,6 +306,14 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
@@ -252,6 +332,14 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<withProps(Relay(ReplyListContainer))
|
||||
asset={
|
||||
@@ -266,6 +354,14 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
<Localized
|
||||
@@ -322,6 +418,14 @@ exports[`when use is logged in renders correctly 1`] = `
|
||||
}
|
||||
}
|
||||
me={Object {}}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<withProps(Relay(ReplyListContainer))
|
||||
asset={
|
||||
@@ -336,6 +440,14 @@ exports[`when use is logged in renders correctly 1`] = `
|
||||
}
|
||||
}
|
||||
me={Object {}}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
@@ -354,6 +466,14 @@ exports[`when use is logged in renders correctly 1`] = `
|
||||
}
|
||||
}
|
||||
me={Object {}}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<withProps(Relay(ReplyListContainer))
|
||||
asset={
|
||||
@@ -368,6 +488,14 @@ exports[`when use is logged in renders correctly 1`] = `
|
||||
}
|
||||
}
|
||||
me={Object {}}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
|
||||
@@ -30,6 +30,12 @@ it("renders username and body", () => {
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
indentLevel: 1,
|
||||
showAuthPopup: noop as any,
|
||||
setCommentID: noop as any,
|
||||
@@ -61,6 +67,12 @@ it("renders body only", () => {
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
indentLevel: 1,
|
||||
showAuthPopup: noop as any,
|
||||
setCommentID: noop as any,
|
||||
@@ -90,6 +102,12 @@ it("hide reply button", () => {
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
indentLevel: 1,
|
||||
showAuthPopup: noop as any,
|
||||
setCommentID: noop as any,
|
||||
@@ -107,6 +125,13 @@ it("shows conversation link", () => {
|
||||
asset: {
|
||||
url: "http://localhost/asset",
|
||||
},
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up",
|
||||
label: "Respect",
|
||||
labelActive: "Respected",
|
||||
},
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
author: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { PropTypesOf } from "talk-framework/types";
|
||||
import { CommentContainer_asset as AssetData } from "talk-stream/__generated__/CommentContainer_asset.graphql";
|
||||
import { CommentContainer_comment as CommentData } from "talk-stream/__generated__/CommentContainer_comment.graphql";
|
||||
import { CommentContainer_me as MeData } from "talk-stream/__generated__/CommentContainer_me.graphql";
|
||||
import { CommentContainer_settings as SettingsData } from "talk-stream/__generated__/CommentContainer_settings.graphql";
|
||||
import {
|
||||
SetCommentIDMutation,
|
||||
ShowAuthPopupMutation,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
withShowAuthPopupMutation,
|
||||
} from "talk-stream/mutations";
|
||||
|
||||
import ReactionButtonContainer from "talk-stream/tabs/comments/containers/ReactionButtonContainer";
|
||||
import { Button } from "talk-ui/components";
|
||||
import Comment, {
|
||||
ButtonsBar,
|
||||
@@ -30,6 +32,7 @@ interface InnerProps {
|
||||
me: MeData | null;
|
||||
comment: CommentData;
|
||||
asset: AssetData;
|
||||
settings: SettingsData;
|
||||
indentLevel?: number;
|
||||
showAuthPopup: ShowAuthPopupMutation;
|
||||
setCommentID: SetCommentIDMutation;
|
||||
@@ -131,6 +134,7 @@ export class CommentContainer extends Component<InnerProps, State> {
|
||||
public render() {
|
||||
const {
|
||||
comment,
|
||||
settings,
|
||||
asset,
|
||||
indentLevel,
|
||||
localReply,
|
||||
@@ -182,6 +186,12 @@ export class CommentContainer extends Component<InnerProps, State> {
|
||||
/>
|
||||
)}
|
||||
<PermalinkButtonContainer commentID={comment.id} />
|
||||
{this.props.me && (
|
||||
<ReactionButtonContainer
|
||||
comment={comment}
|
||||
settings={settings}
|
||||
/>
|
||||
)}
|
||||
</ButtonsBar>
|
||||
{showConversationLink && (
|
||||
<ShowConversationLink
|
||||
@@ -241,6 +251,12 @@ const enhanced = withSetCommentIDMutation(
|
||||
pending
|
||||
...ReplyCommentFormContainer_comment
|
||||
...EditCommentFormContainer_comment
|
||||
...ReactionButtonContainer_comment
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment CommentContainer_settings on Settings {
|
||||
...ReactionButtonContainer_settings
|
||||
}
|
||||
`,
|
||||
})(CommentContainer)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PropTypesOf } from "talk-framework/types";
|
||||
import { LocalReplyListContainer_asset as AssetData } from "talk-stream/__generated__/LocalReplyListContainer_asset.graphql";
|
||||
import { LocalReplyListContainer_comment as CommentData } from "talk-stream/__generated__/LocalReplyListContainer_comment.graphql";
|
||||
import { LocalReplyListContainer_me as MeData } from "talk-stream/__generated__/LocalReplyListContainer_me.graphql";
|
||||
import { LocalReplyListContainer_settings as SettingsData } from "talk-stream/__generated__/LocalReplyListContainer_settings.graphql";
|
||||
|
||||
import ReplyList from "../components/ReplyList";
|
||||
|
||||
@@ -14,6 +15,7 @@ interface InnerProps {
|
||||
me: MeData;
|
||||
asset: AssetData;
|
||||
comment: CommentData;
|
||||
settings: SettingsData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,6 +32,7 @@ export class LocalReplyListContainer extends Component<InnerProps> {
|
||||
return (
|
||||
<ReplyList
|
||||
me={this.props.me}
|
||||
settings={this.props.settings}
|
||||
comment={this.props.comment}
|
||||
comments={this.props.comment.localReplies}
|
||||
asset={this.props.asset}
|
||||
@@ -60,6 +63,11 @@ const enhanced = withFragmentContainer<InnerProps>({
|
||||
}
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment LocalReplyListContainer_settings on Settings {
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
})(LocalReplyListContainer);
|
||||
|
||||
export type LocalReplyListContainerProps = PropTypesOf<typeof enhanced>;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { PermalinkViewContainer_asset as AssetData } from "talk-stream/__generated__/PermalinkViewContainer_asset.graphql";
|
||||
import { PermalinkViewContainer_comment as CommentData } from "talk-stream/__generated__/PermalinkViewContainer_comment.graphql";
|
||||
import { PermalinkViewContainer_me as MeData } from "talk-stream/__generated__/PermalinkViewContainer_me.graphql";
|
||||
import { PermalinkViewContainer_settings as SettingsData } from "talk-stream/__generated__/PermalinkViewContainer_settings.graphql";
|
||||
import {
|
||||
SetCommentIDMutation,
|
||||
withSetCommentIDMutation,
|
||||
@@ -18,6 +19,7 @@ import PermalinkView from "../components/PermalinkView";
|
||||
interface PermalinkViewContainerProps {
|
||||
comment: CommentData | null;
|
||||
asset: AssetData;
|
||||
settings: SettingsData;
|
||||
me: MeData | null;
|
||||
setCommentID: SetCommentIDMutation;
|
||||
pym: PymChild | undefined;
|
||||
@@ -48,12 +50,13 @@ class PermalinkViewContainer extends React.Component<
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { comment, asset, me } = this.props;
|
||||
const { comment, asset, me, settings } = this.props;
|
||||
return (
|
||||
<PermalinkView
|
||||
me={me}
|
||||
asset={asset}
|
||||
comment={comment}
|
||||
settings={settings}
|
||||
showAllCommentsHref={this.getShowAllCommentsHref()}
|
||||
onShowAllComments={this.showAllComments}
|
||||
/>
|
||||
@@ -82,6 +85,11 @@ const enhanced = withContext(ctx => ({
|
||||
...CommentContainer_me
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment PermalinkViewContainer_settings on Settings {
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
})(PermalinkViewContainer)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { ReactionButtonContainer_comment as CommentData } from "talk-stream/__generated__/ReactionButtonContainer_comment.graphql";
|
||||
import { ReactionButtonContainer_settings as SettingsData } from "talk-stream/__generated__/ReactionButtonContainer_settings.graphql";
|
||||
|
||||
import {
|
||||
CreateCommentReactionMutation,
|
||||
DeleteCommentReactionMutation,
|
||||
withCreateCommentReactionMutation,
|
||||
withDeleteCommentReactionMutation,
|
||||
} from "talk-stream/mutations";
|
||||
import ReactionButton from "talk-stream/tabs/comments/components/ReactionButton";
|
||||
|
||||
interface ReactionButtonContainerProps {
|
||||
createCommentReaction: CreateCommentReactionMutation;
|
||||
deleteCommentReaction: DeleteCommentReactionMutation;
|
||||
comment: CommentData;
|
||||
settings: SettingsData;
|
||||
}
|
||||
|
||||
class ReactionButtonContainer extends React.Component<
|
||||
ReactionButtonContainerProps
|
||||
> {
|
||||
private handleClick = () => {
|
||||
const input = {
|
||||
commentID: this.props.comment.id,
|
||||
};
|
||||
|
||||
const { createCommentReaction, deleteCommentReaction } = this.props;
|
||||
const reacted =
|
||||
this.props.comment.myActionPresence &&
|
||||
this.props.comment.myActionPresence.reaction;
|
||||
|
||||
reacted ? deleteCommentReaction(input) : createCommentReaction(input);
|
||||
};
|
||||
public render() {
|
||||
const {
|
||||
actionCounts: {
|
||||
reaction: { total: totalReactions },
|
||||
},
|
||||
} = this.props.comment;
|
||||
const {
|
||||
reaction: { label, labelActive, icon, iconActive },
|
||||
} = this.props.settings;
|
||||
|
||||
const reacted =
|
||||
this.props.comment.myActionPresence &&
|
||||
this.props.comment.myActionPresence.reaction;
|
||||
|
||||
return (
|
||||
<ReactionButton
|
||||
onClick={this.handleClick}
|
||||
totalReactions={totalReactions}
|
||||
reacted={reacted}
|
||||
label={label}
|
||||
labelActive={labelActive}
|
||||
icon={icon}
|
||||
iconActive={iconActive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withDeleteCommentReactionMutation(
|
||||
withCreateCommentReactionMutation(
|
||||
withFragmentContainer<ReactionButtonContainerProps>({
|
||||
comment: graphql`
|
||||
fragment ReactionButtonContainer_comment on Comment {
|
||||
id
|
||||
myActionPresence {
|
||||
reaction
|
||||
}
|
||||
actionCounts {
|
||||
reaction {
|
||||
total
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ReactionButtonContainer_settings on Settings {
|
||||
reaction {
|
||||
label
|
||||
labelActive
|
||||
icon
|
||||
iconActive
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(ReactionButtonContainer)
|
||||
)
|
||||
);
|
||||
@@ -22,6 +22,12 @@ it("renders correctly", () => {
|
||||
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
relay: {
|
||||
hasMore: noop,
|
||||
isLoading: noop,
|
||||
@@ -49,6 +55,12 @@ it("renders correctly when replies are empty", () => {
|
||||
isLoading: noop,
|
||||
} as any,
|
||||
me: null,
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
indentLevel: 1,
|
||||
ReplyListComponent: undefined,
|
||||
localReply: false,
|
||||
@@ -69,6 +81,12 @@ describe("when has more replies", () => {
|
||||
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
relay: {
|
||||
hasMore: () => true,
|
||||
isLoading: () => false,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PropTypesOf } from "talk-framework/types";
|
||||
import { ReplyListContainer1_asset as AssetData } from "talk-stream/__generated__/ReplyListContainer1_asset.graphql";
|
||||
import { ReplyListContainer1_comment as CommentData } from "talk-stream/__generated__/ReplyListContainer1_comment.graphql";
|
||||
import { ReplyListContainer1_me as MeData } from "talk-stream/__generated__/ReplyListContainer1_me.graphql";
|
||||
import { ReplyListContainer1_settings as SettingsData } from "talk-stream/__generated__/ReplyListContainer1_settings.graphql";
|
||||
import {
|
||||
COMMENT_SORT,
|
||||
ReplyListContainer1PaginationQueryVariables,
|
||||
@@ -14,22 +15,29 @@ import {
|
||||
import { ReplyListContainer5_comment as Comment5Data } from "talk-stream/__generated__/ReplyListContainer5_comment.graphql";
|
||||
|
||||
import { StatelessComponent } from "enzyme";
|
||||
import { FragmentKeys } from "talk-framework/lib/relay/types";
|
||||
import ReplyList from "../components/ReplyList";
|
||||
import LocalReplyListContainer from "./LocalReplyListContainer";
|
||||
|
||||
type UnpackArray<T> = T extends ReadonlyArray<infer U> ? U : any;
|
||||
type ReplyNode5 = UnpackArray<Comment5Data["replies"]["edges"]>["node"];
|
||||
|
||||
export interface InnerProps {
|
||||
export interface BaseProps {
|
||||
me: MeData | null;
|
||||
asset: AssetData;
|
||||
comment: CommentData;
|
||||
settings: SettingsData;
|
||||
relay: RelayPaginationProp;
|
||||
indentLevel: number;
|
||||
ReplyListComponent: React.ComponentType<any> | undefined;
|
||||
localReply: boolean | undefined;
|
||||
}
|
||||
|
||||
export type InnerProps = BaseProps & {
|
||||
ReplyListComponent:
|
||||
| React.ComponentType<{ [P in FragmentKeys<BaseProps>]: any }>
|
||||
| undefined;
|
||||
};
|
||||
|
||||
// TODO: (cvle) This should be autogenerated.
|
||||
interface FragmentVariables {
|
||||
count: number;
|
||||
@@ -50,6 +58,14 @@ export class ReplyListContainer extends React.Component<InnerProps> {
|
||||
}
|
||||
const comments = this.props.comment.replies.edges.map(edge => ({
|
||||
...edge.node,
|
||||
replyListElement: this.props.ReplyListComponent && (
|
||||
<this.props.ReplyListComponent
|
||||
me={this.props.me}
|
||||
comment={edge.node}
|
||||
asset={this.props.asset}
|
||||
settings={this.props.settings}
|
||||
/>
|
||||
),
|
||||
// ReplyListContainer5 contains replyCount.
|
||||
showConversationLink: ((edge.node as any) as ReplyNode5).replyCount > 0,
|
||||
}));
|
||||
@@ -59,11 +75,11 @@ export class ReplyListContainer extends React.Component<InnerProps> {
|
||||
comment={this.props.comment}
|
||||
comments={comments}
|
||||
asset={this.props.asset}
|
||||
settings={this.props.settings}
|
||||
onShowAll={this.showAll}
|
||||
hasMore={this.props.relay.hasMore()}
|
||||
disableShowAll={this.state.disableShowAll}
|
||||
indentLevel={this.props.indentLevel}
|
||||
ReplyListComponent={this.props.ReplyListComponent}
|
||||
localReply={this.props.localReply}
|
||||
/>
|
||||
);
|
||||
@@ -94,9 +110,10 @@ function createReplyListContainer(
|
||||
me: GraphQLTaggedNode;
|
||||
asset: GraphQLTaggedNode;
|
||||
comment: GraphQLTaggedNode;
|
||||
settings: GraphQLTaggedNode;
|
||||
},
|
||||
query: GraphQLTaggedNode,
|
||||
ReplyListComponent?: React.ComponentType<any>,
|
||||
ReplyListComponent?: InnerProps["ReplyListComponent"],
|
||||
localReply?: boolean
|
||||
) {
|
||||
return withProps({ indentLevel, ReplyListComponent, localReply })(
|
||||
@@ -145,6 +162,12 @@ const ReplyListContainer5 = createReplyListContainer(
|
||||
...LocalReplyListContainer_me
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ReplyListContainer5_settings on Settings {
|
||||
...LocalReplyListContainer_settings
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
asset: graphql`
|
||||
fragment ReplyListContainer5_asset on Asset {
|
||||
...CommentContainer_asset
|
||||
@@ -201,6 +224,12 @@ const ReplyListContainer4 = createReplyListContainer(
|
||||
...CommentContainer_me
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ReplyListContainer4_settings on Settings {
|
||||
...ReplyListContainer5_settings
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
asset: graphql`
|
||||
fragment ReplyListContainer4_asset on Asset {
|
||||
...ReplyListContainer5_asset
|
||||
@@ -255,6 +284,12 @@ const ReplyListContainer3 = createReplyListContainer(
|
||||
...CommentContainer_me
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ReplyListContainer3_settings on Settings {
|
||||
...ReplyListContainer4_settings
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
asset: graphql`
|
||||
fragment ReplyListContainer3_asset on Asset {
|
||||
...ReplyListContainer4_asset
|
||||
@@ -309,6 +344,12 @@ const ReplyListContainer2 = createReplyListContainer(
|
||||
...CommentContainer_me
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ReplyListContainer2_settings on Settings {
|
||||
...ReplyListContainer3_settings
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
asset: graphql`
|
||||
fragment ReplyListContainer2_asset on Asset {
|
||||
...ReplyListContainer3_asset
|
||||
@@ -363,6 +404,12 @@ const ReplyListContainer1 = createReplyListContainer(
|
||||
...CommentContainer_me
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ReplyListContainer1_settings on Settings {
|
||||
...ReplyListContainer2_settings
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
asset: graphql`
|
||||
fragment ReplyListContainer1_asset on Asset {
|
||||
...ReplyListContainer2_asset
|
||||
|
||||
@@ -21,6 +21,12 @@ it("renders correctly", () => {
|
||||
},
|
||||
},
|
||||
me: null,
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
relay: {
|
||||
hasMore: noop,
|
||||
isLoading: noop,
|
||||
@@ -41,6 +47,12 @@ describe("when has more comments", () => {
|
||||
},
|
||||
},
|
||||
me: null,
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
},
|
||||
},
|
||||
relay: {
|
||||
hasMore: () => true,
|
||||
isLoading: () => false,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { withPaginationContainer } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { StreamContainer_asset as AssetData } from "talk-stream/__generated__/StreamContainer_asset.graphql";
|
||||
import { StreamContainer_me as MeData } from "talk-stream/__generated__/StreamContainer_me.graphql";
|
||||
import { StreamContainer_settings as SettingsData } from "talk-stream/__generated__/StreamContainer_settings.graphql";
|
||||
import {
|
||||
COMMENT_SORT,
|
||||
StreamContainerPaginationQueryVariables,
|
||||
@@ -14,6 +15,7 @@ import Stream from "../components/Stream";
|
||||
|
||||
interface InnerProps {
|
||||
asset: AssetData;
|
||||
settings: SettingsData;
|
||||
me: MeData | null;
|
||||
relay: RelayPaginationProp;
|
||||
}
|
||||
@@ -38,6 +40,7 @@ export class StreamContainer extends React.Component<InnerProps> {
|
||||
<Stream
|
||||
asset={this.props.asset}
|
||||
comments={comments}
|
||||
settings={this.props.settings}
|
||||
onLoadMore={this.loadMore}
|
||||
hasMore={this.props.relay.hasMore()}
|
||||
disableLoadMore={this.state.disableLoadMore}
|
||||
@@ -105,6 +108,12 @@ const enhanced = withPaginationContainer<
|
||||
...UserBoxContainer_me
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment StreamContainer_settings on Settings {
|
||||
...ReplyListContainer1_settings
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
direction: "forward",
|
||||
|
||||
+80
-1
@@ -2,7 +2,6 @@
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<ReplyList
|
||||
ReplyListComponent={[Function]}
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
@@ -31,10 +30,52 @@ exports[`renders correctly 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
"replyListElement": <ReplyListComponent
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>,
|
||||
"showConversationLink": false,
|
||||
},
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
"replyListElement": <ReplyListComponent
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>,
|
||||
"showConversationLink": false,
|
||||
},
|
||||
]
|
||||
@@ -44,6 +85,14 @@ exports[`renders correctly 1`] = `
|
||||
localReply={false}
|
||||
me={null}
|
||||
onShowAll={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -79,10 +128,12 @@ exports[`when has more replies renders hasMore 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
"replyListElement": undefined,
|
||||
"showConversationLink": false,
|
||||
},
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
"replyListElement": undefined,
|
||||
"showConversationLink": false,
|
||||
},
|
||||
]
|
||||
@@ -93,6 +144,14 @@ exports[`when has more replies renders hasMore 1`] = `
|
||||
localReply={false}
|
||||
me={null}
|
||||
onShowAll={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -126,10 +185,12 @@ exports[`when has more replies when showing all disables show all button 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
"replyListElement": undefined,
|
||||
"showConversationLink": false,
|
||||
},
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
"replyListElement": undefined,
|
||||
"showConversationLink": false,
|
||||
},
|
||||
]
|
||||
@@ -140,6 +201,14 @@ exports[`when has more replies when showing all disables show all button 1`] = `
|
||||
localReply={false}
|
||||
me={null}
|
||||
onShowAll={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -173,10 +242,12 @@ exports[`when has more replies when showing all enable show all button after loa
|
||||
Array [
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
"replyListElement": undefined,
|
||||
"showConversationLink": false,
|
||||
},
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
"replyListElement": undefined,
|
||||
"showConversationLink": false,
|
||||
},
|
||||
]
|
||||
@@ -187,5 +258,13 @@ exports[`when has more replies when showing all enable show all button after loa
|
||||
localReply={false}
|
||||
me={null}
|
||||
onShowAll={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
+32
@@ -35,6 +35,14 @@ exports[`renders correctly 1`] = `
|
||||
disableLoadMore={false}
|
||||
me={null}
|
||||
onLoadMore={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -74,6 +82,14 @@ exports[`when has more comments renders hasMore 1`] = `
|
||||
hasMore={true}
|
||||
me={null}
|
||||
onLoadMore={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -113,6 +129,14 @@ exports[`when has more comments when loading more disables load more button 1`]
|
||||
hasMore={true}
|
||||
me={null}
|
||||
onLoadMore={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -152,5 +176,13 @@ exports[`when has more comments when loading more enable load more button after
|
||||
hasMore={true}
|
||||
me={null}
|
||||
onLoadMore={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -36,6 +36,7 @@ export const render = ({
|
||||
return (
|
||||
<PermalinkViewContainer
|
||||
me={props.me}
|
||||
settings={props.settings}
|
||||
comment={props.comment}
|
||||
asset={props.asset}
|
||||
/>
|
||||
@@ -63,6 +64,9 @@ const PermalinkViewQuery: StatelessComponent<InnerProps> = ({
|
||||
comment(id: $commentID) {
|
||||
...PermalinkViewContainer_comment
|
||||
}
|
||||
settings {
|
||||
...PermalinkViewContainer_settings
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{
|
||||
|
||||
@@ -31,7 +31,13 @@ export const render = ({
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
return <StreamContainer me={props.me} asset={props.asset} />;
|
||||
return (
|
||||
<StreamContainer
|
||||
settings={props.settings}
|
||||
me={props.me}
|
||||
asset={props.asset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Spinner />;
|
||||
@@ -49,6 +55,9 @@ const StreamQuery: StatelessComponent<InnerProps> = ({
|
||||
asset(id: $assetID, url: $assetURL) {
|
||||
...StreamContainer_asset
|
||||
}
|
||||
settings {
|
||||
...StreamContainer_settings
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{
|
||||
|
||||
@@ -323,6 +323,21 @@ exports[`cancel edit: edit canceled 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -359,10 +374,10 @@ exports[`cancel edit: edit canceled 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,6 +412,21 @@ exports[`cancel edit: edit canceled 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -843,10 +873,10 @@ exports[`edit a comment: edit form 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -881,6 +911,21 @@ exports[`edit a comment: edit form 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1327,10 +1372,10 @@ exports[`edit a comment: optimistic response 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1365,6 +1410,21 @@ exports[`edit a comment: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1700,6 +1760,21 @@ exports[`edit a comment: render stream 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1736,10 +1811,10 @@ exports[`edit a comment: render stream 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1774,6 +1849,21 @@ exports[`edit a comment: render stream 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2118,6 +2208,21 @@ exports[`edit a comment: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2154,10 +2259,10 @@ exports[`edit a comment: server response 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2192,6 +2297,21 @@ exports[`edit a comment: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2527,6 +2647,21 @@ exports[`shows expiry message: edit form closed 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2563,10 +2698,10 @@ exports[`shows expiry message: edit form closed 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2601,6 +2736,21 @@ exports[`shows expiry message: edit form closed 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3024,10 +3174,10 @@ exports[`shows expiry message: edit time expired 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3062,6 +3212,21 @@ exports[`shows expiry message: edit time expired 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -282,10 +282,10 @@ exports[`loads more comments 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -356,10 +356,10 @@ exports[`loads more comments 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:14:00.000Z"
|
||||
title="2018-07-06T18:14:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:14:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -688,10 +688,10 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -317,6 +317,21 @@ exports[`post a comment: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -391,6 +406,21 @@ exports[`post a comment: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -427,10 +457,10 @@ exports[`post a comment: optimistic response 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -465,6 +495,21 @@ exports[`post a comment: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -784,6 +829,21 @@ exports[`post a comment: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -858,6 +918,21 @@ exports[`post a comment: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -894,10 +969,10 @@ exports[`post a comment: server response 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -932,6 +1007,21 @@ exports[`post a comment: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1251,6 +1341,21 @@ exports[`renders comment stream 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1287,10 +1392,10 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1325,6 +1430,21 @@ exports[`renders comment stream 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -307,6 +307,21 @@ exports[`post a reply: open reply form 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -385,6 +400,21 @@ exports[`post a reply: open reply form 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -463,6 +493,21 @@ exports[`post a reply: open reply form 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -541,6 +586,21 @@ exports[`post a reply: open reply form 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -619,6 +679,21 @@ exports[`post a reply: open reply form 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -697,6 +772,21 @@ exports[`post a reply: open reply form 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<a
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
|
||||
@@ -1169,6 +1259,21 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1247,6 +1352,21 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1325,6 +1445,21 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1403,6 +1538,21 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1481,6 +1631,21 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1559,6 +1724,21 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<a
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
|
||||
@@ -1773,7 +1953,23 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
/>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-directionRow"
|
||||
/>
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2104,6 +2300,21 @@ exports[`post a reply: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2182,6 +2393,21 @@ exports[`post a reply: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2260,6 +2486,21 @@ exports[`post a reply: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2338,6 +2579,21 @@ exports[`post a reply: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2416,6 +2672,21 @@ exports[`post a reply: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2494,6 +2765,21 @@ exports[`post a reply: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<a
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
|
||||
@@ -2571,7 +2857,23 @@ exports[`post a reply: server response 1`] = `
|
||||
/>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-directionRow"
|
||||
/>
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2902,6 +3204,21 @@ exports[`renders comment stream 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2980,6 +3297,21 @@ exports[`renders comment stream 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3058,6 +3390,21 @@ exports[`renders comment stream 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3136,6 +3483,21 @@ exports[`renders comment stream 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3214,6 +3576,21 @@ exports[`renders comment stream 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3292,6 +3669,21 @@ exports[`renders comment stream 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<a
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantUnderlined"
|
||||
|
||||
@@ -307,6 +307,21 @@ exports[`post a reply: open reply form 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -470,10 +485,10 @@ exports[`post a reply: open reply form 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -508,6 +523,21 @@ exports[`post a reply: open reply form 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -827,6 +857,21 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1042,6 +1087,21 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1080,10 +1140,10 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1118,6 +1178,21 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1437,6 +1512,21 @@ exports[`post a reply: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1515,6 +1605,21 @@ exports[`post a reply: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1553,10 +1658,10 @@ exports[`post a reply: server response 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1591,6 +1696,21 @@ exports[`post a reply: server response 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1910,6 +2030,21 @@ exports[`renders comment stream 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1946,10 +2081,10 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1984,6 +2119,21 @@ exports[`renders comment stream 1`] = `
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Respect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ exports[`renders comment stream 1`] = `
|
||||
role="tab"
|
||||
type="button"
|
||||
>
|
||||
1 Comments
|
||||
2 Comments
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -438,10 +438,10 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:14:00.000Z"
|
||||
title="2018-07-06T18:14:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:14:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -512,10 +512,10 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:14:00.000Z"
|
||||
title="2018-07-06T18:14:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:14:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -588,10 +588,10 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:14:00.000Z"
|
||||
title="2018-07-06T18:14:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:14:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -282,10 +282,10 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -286,10 +286,10 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -648,10 +648,10 @@ exports[`show all replies 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
@@ -722,10 +722,10 @@ exports[`show all replies 1`] = `
|
||||
>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:14:00.000Z"
|
||||
title="2018-07-06T18:14:00.000Z"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:14:00.000Z
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import sinon from "sinon";
|
||||
import timekeeper from "timekeeper";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createSinonStub } from "talk-framework/testHelpers";
|
||||
|
||||
import { assets, users } from "../fixtures";
|
||||
import { assets, settings, users } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
function createTestRenderer() {
|
||||
@@ -16,10 +17,8 @@ function createTestRenderer() {
|
||||
.withArgs(undefined, { id: assets[0].id, url: null })
|
||||
.returns(assets[0])
|
||||
),
|
||||
me: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs(undefined).returns(users[0])
|
||||
),
|
||||
me: sinon.stub().returns(users[0]),
|
||||
settings: sinon.stub().returns(settings),
|
||||
},
|
||||
Mutation: {
|
||||
editComment: createSinonStub(
|
||||
|
||||
@@ -4,7 +4,7 @@ import sinon from "sinon";
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createSinonStub } from "talk-framework/testHelpers";
|
||||
|
||||
import { assets, comments } from "../fixtures";
|
||||
import { assets, comments, settings } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
@@ -66,6 +66,7 @@ beforeEach(() => {
|
||||
)
|
||||
.returns(assetStub)
|
||||
),
|
||||
settings: sinon.stub().returns(settings),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import sinon from "sinon";
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createSinonStub } from "talk-framework/testHelpers";
|
||||
|
||||
import { assets, comments } from "../fixtures";
|
||||
import { assets, comments, settings } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
@@ -41,6 +41,7 @@ beforeEach(() => {
|
||||
.withArgs(undefined, { id: assetStub.id, url: null })
|
||||
.returns(assetStub)
|
||||
),
|
||||
settings: sinon.stub().returns(settings),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ReactTestRenderer } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
|
||||
import { settings } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
@@ -10,6 +12,7 @@ beforeEach(() => {
|
||||
Query: {
|
||||
comment: () => null,
|
||||
asset: () => null,
|
||||
settings: sinon.stub().returns(settings),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import sinon from "sinon";
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createSinonStub } from "talk-framework/testHelpers";
|
||||
|
||||
import { assets, comments } from "../fixtures";
|
||||
import { assets, comments, settings } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
@@ -38,6 +38,7 @@ beforeEach(() => {
|
||||
.withArgs(undefined, { id: assetStub.id, url: null })
|
||||
.returns(assetStub)
|
||||
),
|
||||
settings: sinon.stub().returns(settings),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { ReactTestRenderer } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
import timekeeper from "timekeeper";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createSinonStub } from "talk-framework/testHelpers";
|
||||
|
||||
import { assets, users } from "../fixtures";
|
||||
import { assets, baseComment, settings, users } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
beforeEach(() => {
|
||||
const resolvers = {
|
||||
Query: {
|
||||
asset: createSinonStub(s => s.throws(), s => s.returns(assets[0])),
|
||||
me: createSinonStub(s => s.throws(), s => s.returns(users[0])),
|
||||
settings: sinon.stub().returns(settings),
|
||||
me: sinon.stub().returns(users[0]),
|
||||
asset: createSinonStub(
|
||||
s => s.throws(),
|
||||
s =>
|
||||
s
|
||||
.withArgs(undefined, { id: assets[0].id, url: null })
|
||||
.returns(assets[0])
|
||||
),
|
||||
},
|
||||
Mutation: {
|
||||
createComment: createSinonStub(
|
||||
@@ -31,15 +39,10 @@ beforeEach(() => {
|
||||
edge: {
|
||||
cursor: null,
|
||||
node: {
|
||||
...baseComment,
|
||||
id: "comment-x",
|
||||
author: users[0],
|
||||
body: "<strong>Hello world! (from server)</strong>",
|
||||
createdAt: "2018-07-06T18:24:00.000Z",
|
||||
editing: {
|
||||
edited: false,
|
||||
editableUntil: "2018-07-06T18:24:30.000Z",
|
||||
},
|
||||
replies: { edges: [], pageInfo: {} },
|
||||
},
|
||||
},
|
||||
clientMutationId: "0",
|
||||
@@ -71,7 +74,7 @@ it("post a comment", async () => {
|
||||
.findByProps({ inputId: "comments-postCommentForm-field" })
|
||||
.props.onChange({ html: "<strong>Hello world!</strong>" });
|
||||
|
||||
timekeeper.freeze(new Date("2018-07-06T18:24:00.000Z"));
|
||||
timekeeper.freeze(new Date(baseComment.createdAt));
|
||||
|
||||
testRenderer.root
|
||||
.findByProps({ id: "comments-postCommentForm-form" })
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import { ReactTestRenderer } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
import timekeeper from "timekeeper";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createSinonStub } from "talk-framework/testHelpers";
|
||||
|
||||
import { assetWithDeepestReplies, users } from "../fixtures";
|
||||
import {
|
||||
assetWithDeepestReplies,
|
||||
baseComment,
|
||||
settings,
|
||||
users,
|
||||
} from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
beforeEach(() => {
|
||||
const resolvers = {
|
||||
Query: {
|
||||
settings: sinon.stub().returns(settings),
|
||||
me: sinon.stub().returns(users[0]),
|
||||
asset: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.returns(assetWithDeepestReplies)
|
||||
s =>
|
||||
s
|
||||
.withArgs(undefined, { id: assetWithDeepestReplies.id, url: null })
|
||||
.returns(assetWithDeepestReplies)
|
||||
),
|
||||
me: createSinonStub(s => s.throws(), s => s.returns(users[0])),
|
||||
},
|
||||
Mutation: {
|
||||
createComment: createSinonStub(
|
||||
@@ -34,18 +44,10 @@ beforeEach(() => {
|
||||
edge: {
|
||||
cursor: null,
|
||||
node: {
|
||||
...baseComment,
|
||||
id: "comment-x",
|
||||
author: users[0],
|
||||
body: "<strong>Hello world! (from server)</strong>",
|
||||
createdAt: "2018-07-06T18:24:00.000Z",
|
||||
replies: {
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
},
|
||||
editing: {
|
||||
edited: false,
|
||||
editableUntil: "2018-07-06T18:24:30.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: "0",
|
||||
@@ -92,7 +94,7 @@ it("post a reply", async () => {
|
||||
})
|
||||
.props.onChange({ html: "<strong>Hello world!</strong>" });
|
||||
|
||||
timekeeper.freeze(new Date("2018-07-06T18:24:00.000Z"));
|
||||
timekeeper.freeze(new Date(baseComment.createdAt));
|
||||
testRenderer.root
|
||||
.findByProps({
|
||||
id: "comments-replyCommentForm-form-comment-with-deepest-replies-5",
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { ReactTestRenderer } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
import timekeeper from "timekeeper";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createSinonStub } from "talk-framework/testHelpers";
|
||||
|
||||
import { assets, users } from "../fixtures";
|
||||
import { assets, baseComment, settings, users } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
beforeEach(() => {
|
||||
const resolvers = {
|
||||
Query: {
|
||||
asset: createSinonStub(s => s.throws(), s => s.returns(assets[0])),
|
||||
me: createSinonStub(s => s.throws(), s => s.returns(users[0])),
|
||||
settings: sinon.stub().returns(settings),
|
||||
me: sinon.stub().returns(users[0]),
|
||||
asset: createSinonStub(
|
||||
s => s.throws(),
|
||||
s =>
|
||||
s
|
||||
.withArgs(undefined, { id: assets[0].id, url: null })
|
||||
.returns(assets[0])
|
||||
),
|
||||
},
|
||||
Mutation: {
|
||||
createComment: createSinonStub(
|
||||
@@ -31,18 +39,10 @@ beforeEach(() => {
|
||||
edge: {
|
||||
cursor: null,
|
||||
node: {
|
||||
...baseComment,
|
||||
id: "comment-x",
|
||||
author: users[0],
|
||||
body: "<strong>Hello world! (from server)</strong>",
|
||||
createdAt: "2018-07-06T18:24:00.000Z",
|
||||
replies: {
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
},
|
||||
editing: {
|
||||
edited: false,
|
||||
editableUntil: "2018-07-06T18:24:30.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: "0",
|
||||
@@ -84,7 +84,7 @@ it("post a reply", async () => {
|
||||
.findByProps({ inputId: "comments-replyCommentForm-rte-comment-0" })
|
||||
.props.onChange({ html: "<strong>Hello world!</strong>" });
|
||||
|
||||
timekeeper.freeze(new Date("2018-07-06T18:24:00.000Z"));
|
||||
timekeeper.freeze(new Date(baseComment.createdAt));
|
||||
testRenderer.root
|
||||
.findByProps({ id: "comments-replyCommentForm-form-comment-0" })
|
||||
.props.onSubmit();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ReactTestRenderer } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createSinonStub } from "talk-framework/testHelpers";
|
||||
|
||||
import { assetWithDeepReplies } from "../fixtures";
|
||||
import { assetWithDeepReplies, settings } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
@@ -17,6 +18,7 @@ beforeEach(() => {
|
||||
.withArgs(undefined, { id: assetWithDeepReplies.id, url: null })
|
||||
.returns(assetWithDeepReplies)
|
||||
),
|
||||
settings: sinon.stub().returns(settings),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ReactTestRenderer } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createSinonStub } from "talk-framework/testHelpers";
|
||||
|
||||
import { assets } from "../fixtures";
|
||||
import { assets, settings } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
@@ -17,6 +18,7 @@ beforeEach(() => {
|
||||
.withArgs(undefined, { id: assets[0].id, url: null })
|
||||
.returns(assets[0])
|
||||
),
|
||||
settings: sinon.stub().returns(settings),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import sinon from "sinon";
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createSinonStub } from "talk-framework/testHelpers";
|
||||
|
||||
import { assets, comments } from "../fixtures";
|
||||
import { assets, comments, settings } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
@@ -65,6 +65,7 @@ beforeEach(() => {
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
settings: sinon.stub().returns(settings),
|
||||
comment: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs(undefined, { id: commentStub.id }).returns(commentStub)
|
||||
|
||||
@@ -4,7 +4,7 @@ import sinon from "sinon";
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createSinonStub } from "talk-framework/testHelpers";
|
||||
|
||||
import { assetWithDeepestReplies, comments } from "../fixtures";
|
||||
import { assetWithDeepestReplies, comments, settings } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
@@ -25,6 +25,7 @@ beforeEach(() => {
|
||||
id: "comment-with-deepest-replies-5",
|
||||
})
|
||||
),
|
||||
settings: sinon.stub().returns(settings),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
export const settings = {
|
||||
reaction: {
|
||||
icon: "thumb_up",
|
||||
label: "Respect",
|
||||
labelActive: "Respected",
|
||||
},
|
||||
};
|
||||
|
||||
export const users = [
|
||||
{
|
||||
id: "user-0",
|
||||
@@ -13,106 +21,67 @@ export const users = [
|
||||
},
|
||||
];
|
||||
|
||||
export const baseComment = {
|
||||
author: users[0],
|
||||
body: "Comment Body",
|
||||
createdAt: "2018-07-06T18:24:00.000Z",
|
||||
replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } },
|
||||
replyCount: 0,
|
||||
editing: {
|
||||
edited: false,
|
||||
editableUntil: "2018-07-06T18:24:30.000Z",
|
||||
},
|
||||
actionCounts: {
|
||||
reaction: {
|
||||
total: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const comments = [
|
||||
{
|
||||
...baseComment,
|
||||
id: "comment-0",
|
||||
author: users[0],
|
||||
body: "Joining Too",
|
||||
createdAt: "2018-07-06T18:24:00.000Z",
|
||||
replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } },
|
||||
replyCount: 0,
|
||||
editing: {
|
||||
edited: false,
|
||||
editableUntil: "2018-07-06T18:24:30.000Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseComment,
|
||||
id: "comment-1",
|
||||
author: users[1],
|
||||
body: "What's up?",
|
||||
createdAt: "2018-07-06T18:20:00.000Z",
|
||||
replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } },
|
||||
replyCount: 0,
|
||||
editing: {
|
||||
edited: false,
|
||||
editableUntil: "2018-07-06T18:20:30.000Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseComment,
|
||||
id: "comment-2",
|
||||
author: users[2],
|
||||
body: "Hey!",
|
||||
createdAt: "2018-07-06T18:14:00.000Z",
|
||||
replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } },
|
||||
replyCount: 0,
|
||||
editing: {
|
||||
edited: false,
|
||||
editableUntil: "2018-07-06T18:14:30.000Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseComment,
|
||||
id: "comment-3",
|
||||
author: users[2],
|
||||
body: "Comment Body 3",
|
||||
createdAt: "2018-07-06T18:14:00.000Z",
|
||||
replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } },
|
||||
replyCount: 0,
|
||||
editing: {
|
||||
edited: false,
|
||||
editableUntil: "2018-07-06T18:14:30.000Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseComment,
|
||||
id: "comment-4",
|
||||
author: users[2],
|
||||
body: "Comment Body 4",
|
||||
createdAt: "2018-07-06T18:14:00.000Z",
|
||||
replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } },
|
||||
replyCount: 0,
|
||||
editing: {
|
||||
edited: false,
|
||||
editableUntil: "2018-07-06T18:14:30.000Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseComment,
|
||||
id: "comment-5",
|
||||
author: users[2],
|
||||
body: "Comment Body 5",
|
||||
createdAt: "2018-07-06T18:14:00.000Z",
|
||||
replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } },
|
||||
replyCount: 0,
|
||||
editing: {
|
||||
edited: false,
|
||||
editableUntil: "2018-07-06T18:14:30.000Z",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const assets = [
|
||||
{
|
||||
id: "asset-1",
|
||||
url: "http://localhost/assets/asset-1",
|
||||
isClosed: false,
|
||||
commentCounts: {
|
||||
totalVisible: 2,
|
||||
},
|
||||
comments: {
|
||||
edges: [
|
||||
{ node: comments[0], cursor: comments[0].createdAt },
|
||||
{ node: comments[1], cursor: comments[1].createdAt },
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const commentWithReplies = {
|
||||
...baseComment,
|
||||
id: "comment-with-replies",
|
||||
author: users[0],
|
||||
body: "I like yoghurt",
|
||||
createdAt: "2018-07-06T18:24:00.000Z",
|
||||
replies: {
|
||||
edges: [
|
||||
{ node: comments[3], cursor: comments[3].createdAt },
|
||||
@@ -123,17 +92,13 @@ export const commentWithReplies = {
|
||||
},
|
||||
},
|
||||
replyCount: 2,
|
||||
editing: {
|
||||
edited: false,
|
||||
editableUntil: "2018-07-06T18:24:30.000Z",
|
||||
},
|
||||
};
|
||||
|
||||
export const commentWithDeepReplies = {
|
||||
...baseComment,
|
||||
id: "comment-with-deep-replies",
|
||||
author: users[0],
|
||||
body: "I like yoghurt",
|
||||
createdAt: "2018-07-06T18:24:00.000Z",
|
||||
replies: {
|
||||
edges: [
|
||||
{ node: commentWithReplies, cursor: commentWithReplies.createdAt },
|
||||
@@ -144,120 +109,76 @@ export const commentWithDeepReplies = {
|
||||
},
|
||||
},
|
||||
replyCount: 2,
|
||||
editing: {
|
||||
edited: false,
|
||||
editableUntil: "2018-07-06T18:24:30.000Z",
|
||||
},
|
||||
};
|
||||
|
||||
export const assetWithReplies = {
|
||||
id: "asset-with-replies",
|
||||
url: "http://localhost/assets/asset-with-replies",
|
||||
isClosed: false,
|
||||
comments: {
|
||||
edges: [
|
||||
{ node: comments[0], cursor: comments[0].createdAt },
|
||||
{ node: commentWithReplies, cursor: commentWithReplies.createdAt },
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
commentCounts: {
|
||||
totalVisible: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const assetWithDeepReplies = {
|
||||
id: "asset-with-deep-replies",
|
||||
url: "http://localhost/assets/asset-with-replies",
|
||||
isClosed: false,
|
||||
comments: {
|
||||
edges: [
|
||||
{ node: comments[0], cursor: comments[0].createdAt },
|
||||
{
|
||||
node: commentWithDeepReplies,
|
||||
cursor: commentWithDeepReplies.createdAt,
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
commentCounts: {
|
||||
totalVisible: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const commentWithDeepestReplies = {
|
||||
...commentWithReplies,
|
||||
...baseComment,
|
||||
id: "comment-with-deepest-replies",
|
||||
body: "body 0",
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
...commentWithReplies.replies,
|
||||
...baseComment.replies,
|
||||
edges: [
|
||||
{
|
||||
cursor: commentWithReplies.createdAt,
|
||||
cursor: baseComment.createdAt,
|
||||
node: {
|
||||
...commentWithReplies,
|
||||
...baseComment,
|
||||
id: "comment-with-deepest-replies-1",
|
||||
body: "body 1",
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
...commentWithReplies.replies,
|
||||
...baseComment.replies,
|
||||
edges: [
|
||||
{
|
||||
cursor: commentWithReplies.createdAt,
|
||||
cursor: baseComment.createdAt,
|
||||
node: {
|
||||
...commentWithReplies,
|
||||
...baseComment,
|
||||
id: "comment-with-deepest-replies-2",
|
||||
body: "body 2",
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
...commentWithReplies.replies,
|
||||
...baseComment.replies,
|
||||
edges: [
|
||||
{
|
||||
cursor: commentWithReplies.createdAt,
|
||||
cursor: baseComment.createdAt,
|
||||
node: {
|
||||
...commentWithReplies,
|
||||
...baseComment,
|
||||
id: "comment-with-deepest-replies-3",
|
||||
body: "body 3",
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
...commentWithReplies.replies,
|
||||
...baseComment.replies,
|
||||
edges: [
|
||||
{
|
||||
cursor: commentWithReplies.createdAt,
|
||||
cursor: baseComment.createdAt,
|
||||
node: {
|
||||
...commentWithReplies,
|
||||
...baseComment,
|
||||
id: "comment-with-deepest-replies-4",
|
||||
body: "body 4",
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
...commentWithReplies.replies,
|
||||
...baseComment.replies,
|
||||
edges: [
|
||||
{
|
||||
cursor: commentWithReplies.createdAt,
|
||||
cursor: baseComment.createdAt,
|
||||
node: {
|
||||
...commentWithReplies,
|
||||
...baseComment,
|
||||
id: "comment-with-deepest-replies-5",
|
||||
body: "body 5",
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
...commentWithReplies.replies,
|
||||
...baseComment.replies,
|
||||
edges: [
|
||||
{
|
||||
cursor:
|
||||
commentWithReplies.createdAt,
|
||||
cursor: baseComment.createdAt,
|
||||
node: {
|
||||
...commentWithReplies,
|
||||
...baseComment,
|
||||
id:
|
||||
"comment-with-deepest-replies-6",
|
||||
body: "body 6",
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
...commentWithReplies.replies,
|
||||
...baseComment.replies,
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
@@ -286,10 +207,82 @@ export const commentWithDeepestReplies = {
|
||||
},
|
||||
};
|
||||
|
||||
export const baseAsset = {
|
||||
isClosed: false,
|
||||
comments: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
commentCounts: {
|
||||
totalVisible: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const assets = [
|
||||
{
|
||||
...baseAsset,
|
||||
id: "asset-1",
|
||||
url: "http://localhost/assets/asset-1",
|
||||
comments: {
|
||||
edges: [
|
||||
{ node: comments[0], cursor: comments[0].createdAt },
|
||||
{ node: comments[1], cursor: comments[1].createdAt },
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
commentCounts: {
|
||||
totalVisible: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const assetWithReplies = {
|
||||
...baseAsset,
|
||||
id: "asset-with-replies",
|
||||
url: "http://localhost/assets/asset-with-replies",
|
||||
comments: {
|
||||
edges: [
|
||||
{ node: comments[0], cursor: comments[0].createdAt },
|
||||
{ node: commentWithReplies, cursor: commentWithReplies.createdAt },
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
commentCounts: {
|
||||
totalVisible: 2,
|
||||
},
|
||||
};
|
||||
|
||||
export const assetWithDeepReplies = {
|
||||
...baseAsset,
|
||||
id: "asset-with-deep-replies",
|
||||
url: "http://localhost/assets/asset-with-replies",
|
||||
comments: {
|
||||
edges: [
|
||||
{ node: comments[0], cursor: comments[0].createdAt },
|
||||
{
|
||||
node: commentWithDeepReplies,
|
||||
cursor: commentWithDeepReplies.createdAt,
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
commentCounts: {
|
||||
totalVisible: 2,
|
||||
},
|
||||
};
|
||||
|
||||
export const assetWithDeepestReplies = {
|
||||
...baseAsset,
|
||||
id: "asset-with-deepest-replies",
|
||||
url: "http://localhost/assets/asset-with-replies",
|
||||
isClosed: false,
|
||||
comments: {
|
||||
edges: [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
.root {
|
||||
}
|
||||
|
||||
.popover {
|
||||
background: var(--palette-common-white);
|
||||
border: 1px solid var(--palette-grey-lighter);
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
Reference,
|
||||
RefHandler,
|
||||
} from "react-popper";
|
||||
|
||||
import { withStyles } from "talk-ui/hocs";
|
||||
|
||||
import AriaInfo from "../AriaInfo";
|
||||
import * as styles from "./Popover.css";
|
||||
|
||||
@@ -43,6 +46,7 @@ interface PopoverProps {
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
placement?: Placement;
|
||||
classes: typeof styles;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -50,7 +54,7 @@ interface State {
|
||||
}
|
||||
|
||||
class Popover extends React.Component<PopoverProps> {
|
||||
public static defaultProps = {
|
||||
public static defaultProps: Partial<PopoverProps> = {
|
||||
placement: "top",
|
||||
};
|
||||
public state: State = {
|
||||
@@ -92,56 +96,61 @@ class Popover extends React.Component<PopoverProps> {
|
||||
description,
|
||||
className,
|
||||
placement,
|
||||
classes,
|
||||
} = this.props;
|
||||
|
||||
const { visible } = this.state;
|
||||
const popoverClassName = cn(styles.root, className, {
|
||||
[styles.top]: placement!.startsWith("top"),
|
||||
[styles.left]: placement!.startsWith("left"),
|
||||
[styles.right]: placement!.startsWith("right"),
|
||||
[styles.bottom]: placement!.startsWith("bottom"),
|
||||
const popoverClassName = cn(classes.popover, {
|
||||
[classes.top]: placement!.startsWith("top"),
|
||||
[classes.left]: placement!.startsWith("left"),
|
||||
[classes.right]: placement!.startsWith("right"),
|
||||
[classes.bottom]: placement!.startsWith("bottom"),
|
||||
});
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{(props: PopperArrowProps) =>
|
||||
children({
|
||||
forwardRef: props.ref,
|
||||
toggleVisibility: this.toggleVisibility,
|
||||
visible: this.state.visible,
|
||||
})
|
||||
}
|
||||
</Reference>
|
||||
<Popper placement={placement} eventsEnabled positionFixed={false}>
|
||||
{(props: PopperArrowProps) => (
|
||||
<div
|
||||
id={id}
|
||||
role="popup"
|
||||
aria-labelledby={`${id}-ariainfo`}
|
||||
aria-hidden={!visible}
|
||||
>
|
||||
<AriaInfo id={`${id}-ariainfo`}>{description}</AriaInfo>
|
||||
{visible && (
|
||||
<div
|
||||
style={props.style}
|
||||
className={popoverClassName}
|
||||
ref={props.ref}
|
||||
>
|
||||
{typeof body === "function"
|
||||
? body({
|
||||
toggleVisibility: this.toggleVisibility,
|
||||
visible: this.state.visible,
|
||||
})
|
||||
: body}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Popper>
|
||||
</Manager>
|
||||
<div className={cn(classes.root, className)}>
|
||||
<Manager>
|
||||
<Reference>
|
||||
{(props: PopperArrowProps) =>
|
||||
children({
|
||||
forwardRef: props.ref,
|
||||
toggleVisibility: this.toggleVisibility,
|
||||
visible: this.state.visible,
|
||||
})
|
||||
}
|
||||
</Reference>
|
||||
<Popper placement={placement} eventsEnabled positionFixed={false}>
|
||||
{(props: PopperArrowProps) => (
|
||||
<div
|
||||
id={id}
|
||||
role="popup"
|
||||
aria-labelledby={`${id}-ariainfo`}
|
||||
aria-hidden={!visible}
|
||||
>
|
||||
<AriaInfo id={`${id}-ariainfo`}>{description}</AriaInfo>
|
||||
{visible && (
|
||||
<div
|
||||
style={props.style}
|
||||
className={popoverClassName}
|
||||
ref={props.ref}
|
||||
>
|
||||
{typeof body === "function"
|
||||
? body({
|
||||
toggleVisibility: this.toggleVisibility,
|
||||
visible: this.state.visible,
|
||||
})
|
||||
: body}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Popper>
|
||||
</Manager>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Popover;
|
||||
const enhanced = withStyles(styles)(Popover);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -97,7 +97,7 @@ function setupViews(options: AppOptions) {
|
||||
const { parent } = options;
|
||||
|
||||
// configure the default views directory.
|
||||
const views = path.join(__dirname, "..", "..", "..", "static");
|
||||
const views = path.join(__dirname, "..", "..", "..", "..", "dist", "static");
|
||||
parent.set("views", views);
|
||||
|
||||
// Reconfigure nunjucks.
|
||||
|
||||
@@ -9,15 +9,9 @@ export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
|
||||
// TODO: handle better when we improve errors.
|
||||
if (err.message === "not found") {
|
||||
// TODO: handle better when we improve errors.
|
||||
res
|
||||
.status(404)
|
||||
.send(err.message)
|
||||
.end();
|
||||
res.status(404).send(err.message);
|
||||
} else {
|
||||
// TODO: handle better when we improve errors.
|
||||
res
|
||||
.status(500)
|
||||
.send(err.message)
|
||||
.end();
|
||||
res.status(500).send(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import { RequestHandler } from "express";
|
||||
import { MiddlewareOptions } from "graphql-playground-html";
|
||||
import playground from "graphql-playground-middleware-express";
|
||||
|
||||
export default (options: MiddlewareOptions) => playground(options);
|
||||
export default (options: MiddlewareOptions): RequestHandler => (
|
||||
req,
|
||||
res,
|
||||
next
|
||||
) => {
|
||||
try {
|
||||
playground(options)(req, res, () => {
|
||||
// The playground calls next() when it's not supposed to.
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import serveStatic from "express-static-gzip";
|
||||
import path from "path";
|
||||
|
||||
const staticPath = path.resolve(
|
||||
path.join(__dirname, "..", "..", "..", "..", "static", "assets")
|
||||
path.join(__dirname, "..", "..", "..", "..", "..", "dist", "static", "assets")
|
||||
);
|
||||
|
||||
export default serveStatic(staticPath, { index: false });
|
||||
|
||||
@@ -5,8 +5,13 @@ import {
|
||||
AssetToCommentsArgs,
|
||||
CommentToParentsArgs,
|
||||
CommentToRepliesArgs,
|
||||
GQLActionPresence,
|
||||
GQLCOMMENT_SORT,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
ACTION_ITEM_TYPE,
|
||||
retrieveManyUserActionPresence,
|
||||
} from "talk-server/models/action";
|
||||
import {
|
||||
Comment,
|
||||
retrieveCommentAssetConnection,
|
||||
@@ -38,6 +43,17 @@ export default (ctx: Context) => ({
|
||||
comment: new DataLoader((ids: string[]) =>
|
||||
retrieveManyComments(ctx.mongo, ctx.tenant.id, ids)
|
||||
),
|
||||
retrieveMyActionPresence: new DataLoader<string, GQLActionPresence>(
|
||||
(itemIDs: string[]) =>
|
||||
retrieveManyUserActionPresence(
|
||||
ctx.mongo,
|
||||
ctx.tenant.id,
|
||||
// This should only ever be accessed when a user is logged in.
|
||||
ctx.user!.id,
|
||||
ACTION_ITEM_TYPE.COMMENTS,
|
||||
itemIDs
|
||||
)
|
||||
),
|
||||
forUser: (
|
||||
userID: string,
|
||||
// Apply the graph schema defaults at the loader.
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import {
|
||||
GQLCreateCommentDontAgreeInput,
|
||||
GQLCreateCommentFlagInput,
|
||||
GQLCreateCommentInput,
|
||||
GQLCreateCommentReactionInput,
|
||||
GQLDeleteCommentDontAgreeInput,
|
||||
GQLDeleteCommentFlagInput,
|
||||
GQLDeleteCommentReactionInput,
|
||||
GQLEditCommentInput,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { create, edit } from "talk-server/services/comments";
|
||||
import {
|
||||
createDontAgree,
|
||||
createFlag,
|
||||
createReaction,
|
||||
deleteDontAgree,
|
||||
deleteFlag,
|
||||
deleteReaction,
|
||||
} from "talk-server/services/comments/actions";
|
||||
|
||||
export default (ctx: TenantContext) => ({
|
||||
create: (input: GQLCreateCommentInput) =>
|
||||
@@ -30,4 +44,29 @@ export default (ctx: TenantContext) => ({
|
||||
},
|
||||
ctx.req
|
||||
),
|
||||
createReaction: (input: GQLCreateCommentReactionInput) =>
|
||||
createReaction(ctx.mongo, ctx.tenant, ctx.user!, {
|
||||
item_id: input.commentID,
|
||||
}),
|
||||
deleteReaction: (input: GQLDeleteCommentReactionInput) =>
|
||||
deleteReaction(ctx.mongo, ctx.tenant, ctx.user!, {
|
||||
item_id: input.commentID,
|
||||
}),
|
||||
createDontAgree: (input: GQLCreateCommentDontAgreeInput) =>
|
||||
createDontAgree(ctx.mongo, ctx.tenant, ctx.user!, {
|
||||
item_id: input.commentID,
|
||||
}),
|
||||
deleteDontAgree: (input: GQLDeleteCommentDontAgreeInput) =>
|
||||
deleteDontAgree(ctx.mongo, ctx.tenant, ctx.user!, {
|
||||
item_id: input.commentID,
|
||||
}),
|
||||
createFlag: (input: GQLCreateCommentFlagInput) =>
|
||||
createFlag(ctx.mongo, ctx.tenant, ctx.user!, {
|
||||
item_id: input.commentID,
|
||||
reason: input.reason,
|
||||
}),
|
||||
deleteFlag: (input: GQLDeleteCommentFlagInput) =>
|
||||
deleteFlag(ctx.mongo, ctx.tenant, ctx.user!, {
|
||||
item_id: input.commentID,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GQLAssetTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { decodeActionCounts } from "talk-server/models/action";
|
||||
import { Asset } from "talk-server/models/asset";
|
||||
|
||||
const Asset: GQLAssetTypeResolver<Asset> = {
|
||||
@@ -6,6 +7,7 @@ const Asset: GQLAssetTypeResolver<Asset> = {
|
||||
ctx.loaders.Comments.forAsset(asset.id, input),
|
||||
// TODO: implement this.
|
||||
isClosed: () => false,
|
||||
actionCounts: asset => decodeActionCounts(asset.action_counts),
|
||||
commentCounts: asset => asset.comment_counts,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
GQLComment,
|
||||
GQLCommentTypeResolver,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { decodeActionCounts } from "talk-server/models/action";
|
||||
import { Comment } from "talk-server/models/comment";
|
||||
import { createConnection } from "talk-server/models/connection";
|
||||
|
||||
@@ -24,6 +25,11 @@ const Comment: GQLCommentTypeResolver<Comment> = {
|
||||
comment.reply_count > 0
|
||||
? ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input)
|
||||
: createConnection(),
|
||||
actionCounts: comment => decodeActionCounts(comment.action_counts),
|
||||
myActionPresence: (comment, input, ctx) =>
|
||||
ctx.user
|
||||
? ctx.loaders.Comments.retrieveMyActionPresence.load(comment.id)
|
||||
: null,
|
||||
parentCount: comment =>
|
||||
comment.parent_id ? comment.grandparent_ids.length + 1 : 0,
|
||||
depth: comment =>
|
||||
|
||||
@@ -9,10 +9,10 @@ const Mutation: GQLMutationTypeResolver<void> = {
|
||||
const comment = await ctx.mutators.Comment.create(input);
|
||||
return {
|
||||
edge: {
|
||||
// (cvle)
|
||||
// Depending on the sort we can't determine the accurate cursor
|
||||
// in a performant way, so we return null instead.
|
||||
// It seems that Relay does not directly use this value...
|
||||
// NOTE: (cvle)
|
||||
// Depending on the sort we can't determine the accurate cursor in a
|
||||
// performant way, so we return null instead. It seems that Relay does
|
||||
// not directly use this value.
|
||||
cursor: null,
|
||||
node: comment,
|
||||
},
|
||||
@@ -23,6 +23,30 @@ const Mutation: GQLMutationTypeResolver<void> = {
|
||||
settings: await ctx.mutators.Settings.update(input.settings),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
createCommentReaction: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.createReaction(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
deleteCommentReaction: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.deleteReaction(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
createCommentDontAgree: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.createDontAgree(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
deleteCommentDontAgree: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.deleteDontAgree(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
createCommentFlag: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.createFlag(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
deleteCommentFlag: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.deleteFlag(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
};
|
||||
|
||||
export default Mutation;
|
||||
|
||||
@@ -30,23 +30,181 @@ scalar Cursor
|
||||
## Actions
|
||||
################################################################################
|
||||
|
||||
enum ACTION_TYPE {
|
||||
FLAG
|
||||
DONTAGREE
|
||||
"""
|
||||
COMMENT_FLAG_REPORTED_REASON is a reason that is reported by a User on a
|
||||
Comment.
|
||||
"""
|
||||
enum COMMENT_FLAG_REPORTED_REASON {
|
||||
"""
|
||||
COMMENT_REPORTED_OFFENSIVE is used when a User reported a Comment as being
|
||||
offensive.
|
||||
"""
|
||||
COMMENT_REPORTED_OFFENSIVE
|
||||
|
||||
"""
|
||||
COMMENT_REPORTED_SPAM is used when a User reported a Comment as appearing like
|
||||
spam.
|
||||
"""
|
||||
COMMENT_REPORTED_SPAM
|
||||
}
|
||||
|
||||
enum ACTION_ITEM_TYPE {
|
||||
COMMENTS
|
||||
"""
|
||||
COMMENT_FLAG_DETECTED_REASON is a reason that is detected by the system on a
|
||||
Comment.
|
||||
"""
|
||||
enum COMMENT_FLAG_DETECTED_REASON {
|
||||
"""
|
||||
COMMENT_DETECTED_TOXIC is used when the Comment was detected as being toxic by
|
||||
the system.
|
||||
"""
|
||||
COMMENT_DETECTED_TOXIC
|
||||
|
||||
"""
|
||||
COMMENT_DETECTED_SPAM is used when the Comment was detected as having spam by
|
||||
the system.
|
||||
"""
|
||||
COMMENT_DETECTED_SPAM
|
||||
|
||||
"""
|
||||
COMMENT_DETECTED_BODY_COUNT is used when the Comment was detected as exceeding
|
||||
the body length by the system.
|
||||
"""
|
||||
COMMENT_DETECTED_BODY_COUNT
|
||||
|
||||
"""
|
||||
COMMENT_DETECTED_TRUST is used when the Comment being left was done by a User
|
||||
that has a low karma/trust score.
|
||||
"""
|
||||
COMMENT_DETECTED_TRUST
|
||||
|
||||
"""
|
||||
COMMENT_DETECTED_LINKS is used when the Comment was detected as containing
|
||||
links.
|
||||
"""
|
||||
COMMENT_DETECTED_LINKS
|
||||
|
||||
"""
|
||||
COMMENT_DETECTED_BANNED_WORD is used when the Comment was detected as
|
||||
containing a banned word.
|
||||
"""
|
||||
COMMENT_DETECTED_BANNED_WORD
|
||||
|
||||
"""
|
||||
COMMENT_DETECTED_SUSPECT_WORD is used when the Comment was detected as
|
||||
containing a suspect word.
|
||||
"""
|
||||
COMMENT_DETECTED_SUSPECT_WORD
|
||||
}
|
||||
|
||||
enum ACTION_GROUP {
|
||||
SPAM_COMMENT
|
||||
TOXIC_COMMENT
|
||||
BODY_COUNT
|
||||
TRUST
|
||||
LINKS
|
||||
BANNED_WORD
|
||||
SUSPECT_WORD
|
||||
"""
|
||||
COMMENT_FLAG_REASON is the union of the COMMENT_FLAG_REPORTED_REASON
|
||||
and COMMENT_FLAG_DETECTED_REASON types.
|
||||
"""
|
||||
enum COMMENT_FLAG_REASON {
|
||||
COMMENT_REPORTED_OFFENSIVE
|
||||
COMMENT_REPORTED_SPAM
|
||||
COMMENT_DETECTED_TOXIC
|
||||
COMMENT_DETECTED_SPAM
|
||||
COMMENT_DETECTED_BODY_COUNT
|
||||
COMMENT_DETECTED_TRUST
|
||||
COMMENT_DETECTED_LINKS
|
||||
COMMENT_DETECTED_BANNED_WORD
|
||||
COMMENT_DETECTED_SUSPECT_WORD
|
||||
}
|
||||
|
||||
"""
|
||||
ReactionActionCounts stores all the counts for the counts for the reaction
|
||||
action on a given item.
|
||||
"""
|
||||
type ReactionActionCounts {
|
||||
"""
|
||||
total is the total number of reactions against a given item.
|
||||
"""
|
||||
total: Int!
|
||||
}
|
||||
|
||||
"""
|
||||
DontAgreeActionCounts stores all the counts for the counts for the dontAgree
|
||||
action on a given item.
|
||||
"""
|
||||
type DontAgreeActionCounts {
|
||||
"""
|
||||
total is the total number of dontAgree actions against a given item.
|
||||
"""
|
||||
total: Int!
|
||||
}
|
||||
|
||||
type FlagReasonActionCounts {
|
||||
COMMENT_REPORTED_OFFENSIVE: Int!
|
||||
COMMENT_REPORTED_SPAM: Int!
|
||||
COMMENT_DETECTED_TOXIC: Int!
|
||||
COMMENT_DETECTED_SPAM: Int!
|
||||
COMMENT_DETECTED_BODY_COUNT: Int!
|
||||
COMMENT_DETECTED_TRUST: Int!
|
||||
COMMENT_DETECTED_LINKS: Int!
|
||||
COMMENT_DETECTED_BANNED_WORD: Int!
|
||||
COMMENT_DETECTED_SUSPECT_WORD: Int!
|
||||
}
|
||||
|
||||
"""
|
||||
FlagActionCounts stores all the counts for the counts for the flag action on a
|
||||
given item and the reason counts.
|
||||
"""
|
||||
type FlagActionCounts {
|
||||
"""
|
||||
total is the total number of flags against a given item.
|
||||
"""
|
||||
total: Int!
|
||||
|
||||
"""
|
||||
reasons stores the counts for the various reasons that an item could be
|
||||
flagged for.
|
||||
"""
|
||||
reasons: FlagReasonActionCounts!
|
||||
}
|
||||
|
||||
"""
|
||||
ActionCounts returns the counts of each action for an item.
|
||||
"""
|
||||
type ActionCounts {
|
||||
"""
|
||||
reaction returns the counts for the reaction action on an item.
|
||||
"""
|
||||
reaction: ReactionActionCounts!
|
||||
|
||||
"""
|
||||
dontAgree returns the counts for the dontAgree action on an item. This edge is
|
||||
restricted to administrators and moderators.
|
||||
"""
|
||||
dontAgree: DontAgreeActionCounts! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
flag returns the counts for the flag action on an item. This edge is
|
||||
restricted to administrators and moderators.
|
||||
"""
|
||||
flag: FlagActionCounts! @auth(roles: [ADMIN, MODERATOR])
|
||||
}
|
||||
|
||||
"""
|
||||
ActionPresence returns whether or not a given item has one of the following
|
||||
actions on it. This is typically used to determine if a given user has left
|
||||
one of the following actions.
|
||||
"""
|
||||
type ActionPresence {
|
||||
"""
|
||||
reaction is true when a reaction action was left on an item.
|
||||
"""
|
||||
reaction: Boolean!
|
||||
|
||||
"""
|
||||
dontAgree is true when a dontAgree action was left on an item.
|
||||
"""
|
||||
dontAgree: Boolean!
|
||||
|
||||
"""
|
||||
flag is true when a flag action was left on an item.
|
||||
"""
|
||||
flag: Boolean!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -331,6 +489,42 @@ type Email {
|
||||
fromAddress: String
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## ReactionConfiguration
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
ReactionConfiguration stores the configuration for reactions used across this
|
||||
Tenant.
|
||||
"""
|
||||
type ReactionConfiguration {
|
||||
"""
|
||||
icon is the string representing the icon to be used for the reactions.
|
||||
"""
|
||||
icon: String!
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
iconActive: String
|
||||
|
||||
"""
|
||||
label is the string placed beside the reaction icon to provide better context.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
labelActive is the string placed beside the reaction icon to provide better
|
||||
context when it has been selected.
|
||||
"""
|
||||
labelActive: String
|
||||
|
||||
"""
|
||||
color is the hex color code that can be used to change the color of the button.
|
||||
"""
|
||||
color: String
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Settings
|
||||
################################################################################
|
||||
@@ -439,22 +633,22 @@ type Settings {
|
||||
"""
|
||||
organizationName is the name of the organization.
|
||||
"""
|
||||
organizationName: String
|
||||
organizationName: String!
|
||||
|
||||
"""
|
||||
organizationContactEmail is the email of the organization.
|
||||
"""
|
||||
organizationContactEmail: String
|
||||
organizationContactEmail: String!
|
||||
|
||||
"""
|
||||
email is the set of credentials and settings associated with the organization.
|
||||
"""
|
||||
email: Email @auth(roles: [ADMIN])
|
||||
email: Email! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
wordlist will return a given list of words.
|
||||
"""
|
||||
wordlist: Wordlist @auth(roles: [ADMIN, MODERATOR])
|
||||
wordlist: Wordlist! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
auth contains all the settings related to authentication and authorization.
|
||||
@@ -464,13 +658,18 @@ type Settings {
|
||||
"""
|
||||
integrations contains all the external integrations that can be enabled.
|
||||
"""
|
||||
integrations: ExternalIntegrations @auth(roles: [ADMIN])
|
||||
integrations: ExternalIntegrations! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
karma is the set of settings related to how user Trust and Karma are
|
||||
handled.
|
||||
"""
|
||||
karma: Karma @auth(roles: [ADMIN, MODERATOR])
|
||||
karma: Karma! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
reaction specifies the configuration for reactions.
|
||||
"""
|
||||
reaction: ReactionConfiguration!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -697,6 +896,17 @@ type Comment {
|
||||
"""
|
||||
editing: EditInfo!
|
||||
|
||||
"""
|
||||
actionCounts stores the counts of all the actions for the Comment.
|
||||
"""
|
||||
actionCounts: ActionCounts!
|
||||
|
||||
"""
|
||||
myActionPresence stores the presense information for all the actions
|
||||
left by the current User on this Comment.
|
||||
"""
|
||||
myActionPresence: ActionPresence
|
||||
|
||||
"""
|
||||
asset is the Asset that the Comment was written on.
|
||||
"""
|
||||
@@ -838,6 +1048,12 @@ type Asset {
|
||||
after: Cursor
|
||||
): CommentsConnection!
|
||||
|
||||
"""
|
||||
actionCounts stores the counts of all the actions against this Asset and it's
|
||||
Comments.
|
||||
"""
|
||||
actionCounts: ActionCounts! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
author is the authors listed in the meta tags for the Asset.
|
||||
"""
|
||||
@@ -1388,6 +1604,179 @@ type UpdateSettingsPayload {
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## createCommentReaction
|
||||
##################
|
||||
|
||||
input CreateCommentReactionInput {
|
||||
"""
|
||||
commentID is the Comment's ID that we want to create a Reaction on.
|
||||
"""
|
||||
commentID: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type CreateCommentReactionPayload {
|
||||
"""
|
||||
comment is the Comment that the Reaction was created on.
|
||||
"""
|
||||
comment: Comment
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## deleteCommentReaction
|
||||
##################
|
||||
|
||||
input DeleteCommentReactionInput {
|
||||
"""
|
||||
commentID is the Comment's ID that we want to delete a Reaction on.
|
||||
"""
|
||||
commentID: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type DeleteCommentReactionPayload {
|
||||
"""
|
||||
comment is the Comment that the Reaction was deleted on.
|
||||
"""
|
||||
comment: Comment
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## createCommentDontAgree
|
||||
##################
|
||||
|
||||
input CreateCommentDontAgreeInput {
|
||||
"""
|
||||
commentID is the Comment's ID that we want to create a DontAgree on.
|
||||
"""
|
||||
commentID: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type CreateCommentDontAgreePayload {
|
||||
"""
|
||||
comment is the Comment that the DontAgree was created on.
|
||||
"""
|
||||
comment: Comment
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## deleteCommentDontAgree
|
||||
##################
|
||||
|
||||
input DeleteCommentDontAgreeInput {
|
||||
"""
|
||||
commentID is the Comment's ID that we want to delete a DontAgree on.
|
||||
"""
|
||||
commentID: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type DeleteCommentDontAgreePayload {
|
||||
"""
|
||||
comment is the Comment that the DontAgree was deleted on.
|
||||
"""
|
||||
comment: Comment
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## createCommentFlag
|
||||
##################
|
||||
|
||||
input CreateCommentFlagInput {
|
||||
"""
|
||||
commentID is the Comment's ID that we want to create a Flag on.
|
||||
"""
|
||||
commentID: ID!
|
||||
|
||||
"""
|
||||
reason is the selected reason why the Flag is being created.
|
||||
"""
|
||||
reason: COMMENT_FLAG_REPORTED_REASON!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type CreateCommentFlagPayload {
|
||||
"""
|
||||
comment is the Comment that the Flag was created on.
|
||||
"""
|
||||
comment: Comment
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## deleteCommentFlag
|
||||
##################
|
||||
|
||||
input DeleteCommentFlagInput {
|
||||
"""
|
||||
commentID is the Comment's ID that we want to delete a Flag on.
|
||||
"""
|
||||
commentID: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type DeleteCommentFlagPayload {
|
||||
"""
|
||||
comment is the Comment that the Flag was deleted on.
|
||||
"""
|
||||
comment: Comment
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## Mutation
|
||||
##################
|
||||
@@ -1409,6 +1798,52 @@ type Mutation {
|
||||
"""
|
||||
updateSettings(input: UpdateSettingsInput!): UpdateSettingsPayload
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
createCommentReaction will create a Reaction authored by the current logged in
|
||||
User on a Comment.
|
||||
"""
|
||||
createCommentReaction(
|
||||
input: CreateCommentReactionInput!
|
||||
): CreateCommentReactionPayload @auth
|
||||
|
||||
"""
|
||||
deleteCommentReaction will delete a Reaction authored by the current logged in
|
||||
User on a Comment if it exists.
|
||||
"""
|
||||
deleteCommentReaction(
|
||||
input: CreateCommentReactionInput!
|
||||
): CreateCommentReactionPayload @auth
|
||||
|
||||
"""
|
||||
createCommentDontAgree will create a DontAgree authored by the current logged in
|
||||
User on a Comment.
|
||||
"""
|
||||
createCommentDontAgree(
|
||||
input: CreateCommentDontAgreeInput!
|
||||
): CreateCommentDontAgreePayload @auth
|
||||
|
||||
"""
|
||||
deleteCommentDontAgree will delete a DontAgree authored by the current logged in
|
||||
User on a Comment if it exists.
|
||||
"""
|
||||
deleteCommentDontAgree(
|
||||
input: CreateCommentDontAgreeInput!
|
||||
): CreateCommentDontAgreePayload @auth
|
||||
|
||||
"""
|
||||
createCommentFlag will create a Flag authored by the current logged in User on
|
||||
a given Comment.
|
||||
"""
|
||||
createCommentFlag(input: CreateCommentFlagInput!): CreateCommentFlagPayload
|
||||
@auth
|
||||
|
||||
"""
|
||||
deleteCommentFlag will create a Flag authored by the current logged in User on
|
||||
a given Comment.
|
||||
"""
|
||||
deleteCommentFlag(input: DeleteCommentFlagInput!): DeleteCommentFlagPayload
|
||||
@auth
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
GQLCOMMENT_FLAG_REASON,
|
||||
GQLFlagReasonActionCounts,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
type ExtractKeys<T> = { [P in keyof T]: P }[keyof T];
|
||||
type A = ExtractKeys<typeof GQLCOMMENT_FLAG_REASON>;
|
||||
type B = ExtractKeys<GQLFlagReasonActionCounts>;
|
||||
|
||||
// These tests ensure that the enums contained in GQLCOMMENT_FLAG_REASON are
|
||||
// also defined on the GQLFlagReasonActionCounts type.
|
||||
describe("GQLFlagReasonActionCounts", () => {
|
||||
it("contains all the flag enum types", () => {
|
||||
const a: A = "" as any;
|
||||
let b: B = "" as any;
|
||||
b = a;
|
||||
expect(b).toBe("");
|
||||
|
||||
let c: A = "" as any;
|
||||
const d: B = "" as any;
|
||||
c = d;
|
||||
expect(c).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`#decodeActionCounts parses the action counts correctly 1`] = `
|
||||
Object {
|
||||
"DONT_AGREE": 1,
|
||||
"FLAG": 2,
|
||||
"FLAG__COMMENT_DETECTED_BANNED_WORD": 1,
|
||||
"FLAG__COMMENT_DETECTED_BODY_COUNT": 1,
|
||||
"REACTION": 3,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#decodeActionCounts parses the action counts correctly 2`] = `
|
||||
Object {
|
||||
"dontAgree": Object {
|
||||
"total": 1,
|
||||
},
|
||||
"flag": Object {
|
||||
"reasons": Object {
|
||||
"COMMENT_DETECTED_BANNED_WORD": 1,
|
||||
"COMMENT_DETECTED_BODY_COUNT": 1,
|
||||
"COMMENT_DETECTED_LINKS": 0,
|
||||
"COMMENT_DETECTED_SPAM": 0,
|
||||
"COMMENT_DETECTED_SUSPECT_WORD": 0,
|
||||
"COMMENT_DETECTED_TOXIC": 0,
|
||||
"COMMENT_DETECTED_TRUST": 0,
|
||||
"COMMENT_REPORTED_OFFENSIVE": 0,
|
||||
"COMMENT_REPORTED_SPAM": 0,
|
||||
},
|
||||
"total": 2,
|
||||
},
|
||||
"reaction": Object {
|
||||
"total": 3,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#encodeActionCounts generates the action counts correctly 1`] = `
|
||||
Object {
|
||||
"DONT_AGREE": 1,
|
||||
"FLAG": 2,
|
||||
"FLAG__COMMENT_DETECTED_BANNED_WORD": 1,
|
||||
"FLAG__COMMENT_DETECTED_BODY_COUNT": 1,
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,132 @@
|
||||
import { GQLCOMMENT_FLAG_REASON } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
Action,
|
||||
ACTION_ITEM_TYPE,
|
||||
ACTION_TYPE,
|
||||
decodeActionCounts,
|
||||
encodeActionCounts,
|
||||
validateAction,
|
||||
} from "talk-server/models/action";
|
||||
|
||||
describe("#encodeActionCounts", () => {
|
||||
it("generates the action counts correctly", () => {
|
||||
const actions = [
|
||||
{ action_type: ACTION_TYPE.DONT_AGREE },
|
||||
{
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BANNED_WORD,
|
||||
},
|
||||
{
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BODY_COUNT,
|
||||
},
|
||||
];
|
||||
const actionCounts = encodeActionCounts(...(actions as Action[]));
|
||||
|
||||
expect(actionCounts).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#decodeActionCounts", () => {
|
||||
it("parses the action counts correctly", () => {
|
||||
const actions = [
|
||||
{ action_type: ACTION_TYPE.REACTION },
|
||||
{ action_type: ACTION_TYPE.REACTION },
|
||||
{ action_type: ACTION_TYPE.REACTION },
|
||||
{ action_type: ACTION_TYPE.DONT_AGREE },
|
||||
{
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BANNED_WORD,
|
||||
},
|
||||
{
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BODY_COUNT,
|
||||
},
|
||||
];
|
||||
|
||||
const modelActionCounts = encodeActionCounts(...(actions as Action[]));
|
||||
|
||||
expect(modelActionCounts).toMatchSnapshot();
|
||||
|
||||
const actionCounts = decodeActionCounts(modelActionCounts);
|
||||
|
||||
expect(actionCounts).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#validateAction", () => {
|
||||
it("allows a valid action", () => {
|
||||
const actions = [
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.REACTION,
|
||||
},
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.DONT_AGREE,
|
||||
},
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_SPAM,
|
||||
},
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_TOXIC,
|
||||
},
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BODY_COUNT,
|
||||
},
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_TRUST,
|
||||
},
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_LINKS,
|
||||
},
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BANNED_WORD,
|
||||
},
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_SUSPECT_WORD,
|
||||
},
|
||||
];
|
||||
|
||||
for (const action of actions) {
|
||||
validateAction(action as Action);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not allow an invalid action", () => {
|
||||
const actions = [
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.DONT_AGREE,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_SPAM,
|
||||
},
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.DONT_AGREE,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BODY_COUNT,
|
||||
},
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
},
|
||||
];
|
||||
|
||||
for (const action of actions) {
|
||||
expect(() => validateAction(action as Action)).toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,511 @@
|
||||
import Joi from "joi";
|
||||
import { camelCase, pick } from "lodash";
|
||||
import { Db } from "mongodb";
|
||||
import uuid from "uuid";
|
||||
|
||||
import { Omit, Sub } from "talk-common/types";
|
||||
import {
|
||||
GQLActionCounts,
|
||||
GQLActionPresence,
|
||||
GQLCOMMENT_FLAG_DETECTED_REASON,
|
||||
GQLCOMMENT_FLAG_REASON,
|
||||
GQLCOMMENT_FLAG_REPORTED_REASON,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { FilterQuery } from "talk-server/models/query";
|
||||
import { TenantResource } from "talk-server/models/tenant";
|
||||
|
||||
function collection(db: Db) {
|
||||
return db.collection<Readonly<Action>>("actions");
|
||||
}
|
||||
|
||||
export enum ACTION_TYPE {
|
||||
/**
|
||||
* REACTION corresponds to a reaction to a comment from a user.
|
||||
*/
|
||||
REACTION = "REACTION",
|
||||
|
||||
/**
|
||||
* DONT_AGREE corresponds to when a user marks a given comment that they don't
|
||||
* agree with.
|
||||
*/
|
||||
DONT_AGREE = "DONT_AGREE",
|
||||
|
||||
/**
|
||||
* FLAG corresponds to a flag action that indicates that the given resource needs
|
||||
* moderator attention.
|
||||
*/
|
||||
FLAG = "FLAG",
|
||||
}
|
||||
|
||||
export type EncodedActionCounts = Record<string, number>;
|
||||
|
||||
export interface ActionCountGroup {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export enum ACTION_ITEM_TYPE {
|
||||
COMMENTS = "COMMENTS",
|
||||
}
|
||||
|
||||
/**
|
||||
* FLAG_REASON is the reason that a given Flag has been created.
|
||||
*/
|
||||
export type FLAG_REASON =
|
||||
| GQLCOMMENT_FLAG_DETECTED_REASON
|
||||
| GQLCOMMENT_FLAG_REPORTED_REASON
|
||||
| GQLCOMMENT_FLAG_REASON;
|
||||
|
||||
export interface Action extends TenantResource {
|
||||
readonly id: string;
|
||||
action_type: ACTION_TYPE;
|
||||
item_type: ACTION_ITEM_TYPE;
|
||||
item_id: string;
|
||||
reason?: FLAG_REASON;
|
||||
user_id?: string;
|
||||
created_at: Date;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
const ActionSchema = [
|
||||
// Flags
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
// Only reasons for the flag action will be allowed here, and it must be
|
||||
// specified.
|
||||
reason: Object.keys(GQLCOMMENT_FLAG_REASON),
|
||||
},
|
||||
// Don't Agree
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.DONT_AGREE,
|
||||
},
|
||||
// Reaction
|
||||
{
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
action_type: ACTION_TYPE.REACTION,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* validateAction is used to validate that a specific action conforms to the
|
||||
* expected schema, `ActionSchema`.
|
||||
*/
|
||||
export function validateAction(
|
||||
action: Pick<Action, "item_type" | "action_type" | "reason">
|
||||
) {
|
||||
const { error } = Joi.validate(
|
||||
// In typescript, this isn't an issue, but when this is transpiled to
|
||||
// javascript, it will contain additional elements.
|
||||
pick(action, ["item_type", "action_type", "reason"]),
|
||||
ActionSchema,
|
||||
{
|
||||
presence: "required",
|
||||
abortEarly: false,
|
||||
}
|
||||
);
|
||||
if (error) {
|
||||
// TODO: wrap error?
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export type CreateActionInput = Omit<Action, "id" | "tenant_id" | "created_at">;
|
||||
|
||||
export interface CreateActionResultObject {
|
||||
/**
|
||||
* action contains the resultant action that was created.
|
||||
*/
|
||||
action: Action;
|
||||
|
||||
/**
|
||||
* wasUpserted when true, indicates that this action was just newly created.
|
||||
* When false, it indicates that this action was just looked up, and had
|
||||
* existed prior to the `createAction` call.
|
||||
*/
|
||||
wasUpserted: boolean;
|
||||
}
|
||||
|
||||
export async function createAction(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
input: CreateActionInput
|
||||
): Promise<CreateActionResultObject> {
|
||||
// Create a new ID for the action.
|
||||
const id = uuid.v4();
|
||||
|
||||
// defaults are the properties set by the application when a new action is
|
||||
// created.
|
||||
const defaults: Sub<Action, CreateActionInput> = {
|
||||
id,
|
||||
tenant_id: tenantID,
|
||||
created_at: new Date(),
|
||||
};
|
||||
|
||||
// Merge the defaults with the input.
|
||||
const action: Readonly<Action> = {
|
||||
...defaults,
|
||||
...input,
|
||||
};
|
||||
|
||||
// This filter ensures that a given user can't flag/respect a given user more
|
||||
// than once.
|
||||
const filter: FilterQuery<Action> = {
|
||||
action_type: input.action_type,
|
||||
item_type: input.item_type,
|
||||
item_id: input.item_id,
|
||||
reason: input.reason,
|
||||
user_id: input.user_id,
|
||||
};
|
||||
|
||||
// Create the upsert/update operation.
|
||||
const update: { $setOnInsert: Readonly<Action> } = {
|
||||
$setOnInsert: action,
|
||||
};
|
||||
|
||||
// Insert the action into the database using an upsert operation.
|
||||
const result = await collection(mongo).findOneAndUpdate(filter, update, {
|
||||
// We are using this to create a action, so we need to upsert it.
|
||||
upsert: true,
|
||||
|
||||
// False to return the updated document instead of the original document.
|
||||
// This lets us detect if the document was updated or not.
|
||||
returnOriginal: false,
|
||||
});
|
||||
|
||||
// Check to see if this was a new action that was upserted, or one was found
|
||||
// that matched existing records. We are sure here that the record exists
|
||||
// because we're returning the updated document and performing an upsert
|
||||
// operation.
|
||||
|
||||
// Because it's relevant that we know that the action was just created, or
|
||||
// was just looked up, we need to return the action with an object that
|
||||
// indicates if it was upserted.
|
||||
const wasUpserted = result.value!.id === id;
|
||||
|
||||
// Return the action that was created/found with a boolean indicating if this
|
||||
// action was just upserted (and therefore was newly created).
|
||||
return {
|
||||
action: result.value!,
|
||||
wasUpserted,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createActions(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
inputs: CreateActionInput[]
|
||||
): Promise<CreateActionResultObject[]> {
|
||||
// TODO: (wyattjoh) replace with a batch write.
|
||||
return Promise.all(inputs.map(input => createAction(mongo, tenantID, input)));
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieveManyUserActionPresence returns the action presence for a specific
|
||||
* user.
|
||||
*/
|
||||
export async function retrieveManyUserActionPresence(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
userID: string | null,
|
||||
itemType: ACTION_ITEM_TYPE,
|
||||
itemIDs: string[]
|
||||
): Promise<GQLActionPresence[]> {
|
||||
const cursor = await collection(mongo).find(
|
||||
{
|
||||
tenant_id: tenantID,
|
||||
user_id: userID,
|
||||
item_type: itemType,
|
||||
item_id: { $in: itemIDs },
|
||||
},
|
||||
{
|
||||
// We only need the item_id and action_type from the database.
|
||||
projection: {
|
||||
item_id: 1,
|
||||
action_type: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const actions = await cursor.toArray();
|
||||
|
||||
// For each of the actions returned by the query, group the actions by the
|
||||
// item id. Then compute the action presence for each of the actions.
|
||||
return itemIDs
|
||||
.map(itemID => actions.filter(action => action.item_id === itemID))
|
||||
.map(itemActions =>
|
||||
itemActions.reduce(
|
||||
(actionPresence, { action_type }) => ({
|
||||
...actionPresence,
|
||||
[camelCase(action_type)]: true,
|
||||
}),
|
||||
{
|
||||
reaction: false,
|
||||
dontAgree: false,
|
||||
flag: false,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export type DeleteActionInput = Pick<
|
||||
Action,
|
||||
"action_type" | "item_type" | "item_id" | "reason" | "user_id"
|
||||
>;
|
||||
|
||||
/**
|
||||
* The result returned by `deleteAction`.
|
||||
*/
|
||||
export interface DeletedActionResultObject {
|
||||
/**
|
||||
* action is the action that was deleted.
|
||||
*/
|
||||
action?: Action;
|
||||
|
||||
/**
|
||||
* wasDeleted is true when the action that was supposed to be deleted was
|
||||
* actually deleted.
|
||||
*/
|
||||
wasDeleted: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* deleteAction will delete the action based on the form of the action rather
|
||||
* than a specific action by ID.
|
||||
*/
|
||||
export async function deleteAction(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
input: DeleteActionInput
|
||||
): Promise<DeletedActionResultObject> {
|
||||
// Extract the filter parameters.
|
||||
const filter: FilterQuery<Action> = {
|
||||
tenant_id: tenantID,
|
||||
action_type: input.action_type,
|
||||
item_type: input.item_type,
|
||||
item_id: input.item_id,
|
||||
user_id: input.user_id,
|
||||
};
|
||||
|
||||
// Only add the reason to the filter if it's been specified, otherwise we'll
|
||||
// never match a Flag that has an unspecified reason.
|
||||
if (input.reason) {
|
||||
filter.reason = input.reason;
|
||||
}
|
||||
|
||||
// Remove the action from the database, returning the action that was deleted.
|
||||
const result = await collection(mongo).findOneAndDelete(filter);
|
||||
return {
|
||||
action: result.value,
|
||||
wasDeleted: Boolean(result.ok && result.value),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ACTION_COUNT_JOIN_CHAR is the character that is used to separate the reason
|
||||
* from the action type when storing the action counts in the models.
|
||||
*/
|
||||
export const ACTION_COUNT_JOIN_CHAR = "__";
|
||||
|
||||
/**
|
||||
* encodeActionCounts will take a list of actions, and generate action counts
|
||||
* from it.
|
||||
*
|
||||
* @param actions list of actions to generate the action counts from
|
||||
*/
|
||||
export function encodeActionCounts(...actions: Action[]): EncodedActionCounts {
|
||||
const actionCounts: EncodedActionCounts = {};
|
||||
|
||||
// Loop over the actions, and increment them.
|
||||
for (const action of actions) {
|
||||
for (const key of encodeActionCountKeys(action)) {
|
||||
if (key in actionCounts) {
|
||||
actionCounts[key]++;
|
||||
} else {
|
||||
actionCounts[key] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return actionCounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* invertEncodedActionCounts will allow inverting of the action count object.
|
||||
*
|
||||
* @param actionCounts the encoded action counts to invert
|
||||
*/
|
||||
export function invertEncodedActionCounts(
|
||||
actionCounts: EncodedActionCounts
|
||||
): EncodedActionCounts {
|
||||
for (const key in actionCounts) {
|
||||
if (!actionCounts.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (actionCounts[key] > 0) {
|
||||
actionCounts[key] = -actionCounts[key];
|
||||
}
|
||||
}
|
||||
|
||||
return actionCounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* encodeActionCountKeys encodes the action into string keys which represents
|
||||
* the groupings as seen in `EncodedActionCounts`.
|
||||
*/
|
||||
function encodeActionCountKeys(action: Action): string[] {
|
||||
const keys = [action.action_type as string];
|
||||
if (action.reason) {
|
||||
keys.push(
|
||||
[action.action_type as string, action.reason as string].join(
|
||||
ACTION_COUNT_JOIN_CHAR
|
||||
)
|
||||
);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
interface DecodedActionCountKey {
|
||||
/**
|
||||
* actionType stores the action type referenced by the key.
|
||||
*/
|
||||
actionType: ACTION_TYPE;
|
||||
|
||||
/**
|
||||
* reason stores the reason referenced by the key if the actionType is FLAG.
|
||||
*/
|
||||
reason?: GQLCOMMENT_FLAG_REASON;
|
||||
}
|
||||
|
||||
/**
|
||||
* decodeActionCountGroup will unpack the key as it is encoded into the separate
|
||||
* actionType and reason.
|
||||
*/
|
||||
function decodeActionCountKey(key: string): DecodedActionCountKey {
|
||||
let actionType: string = "";
|
||||
let reason: string = "";
|
||||
|
||||
if (key.indexOf(ACTION_COUNT_JOIN_CHAR) >= 0) {
|
||||
const keys = key.split(ACTION_COUNT_JOIN_CHAR);
|
||||
if (keys.length !== 2) {
|
||||
throw new Error(
|
||||
"invalid action count contained more than two components"
|
||||
);
|
||||
}
|
||||
|
||||
actionType = keys[0];
|
||||
reason = keys[1];
|
||||
|
||||
// Validate that the action type is flag.
|
||||
if (actionType !== ACTION_TYPE.FLAG) {
|
||||
throw new Error("invalid action type, expected only flag to have reason");
|
||||
}
|
||||
|
||||
// Validate that the reason is valid.
|
||||
if (!reason || !(reason in GQLCOMMENT_FLAG_REASON)) {
|
||||
throw new Error("expected flag to have a reason that was valid");
|
||||
}
|
||||
} else {
|
||||
actionType = key;
|
||||
}
|
||||
|
||||
// Validate that the action type is valid.
|
||||
if (!actionType || !(actionType in ACTION_TYPE)) {
|
||||
throw new Error("expected action to have an action type that was valid");
|
||||
}
|
||||
|
||||
const result: DecodedActionCountKey = {
|
||||
actionType: actionType as ACTION_TYPE,
|
||||
};
|
||||
|
||||
// Merge in the reason if it's provided. If we got here, we know that the
|
||||
// reason is a GQLCOMMENT_FLAG_REASON.
|
||||
if (reason) {
|
||||
result.reason = reason as GQLCOMMENT_FLAG_REASON;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* createEmptyActionCounts creates a default/empty set of action counts.
|
||||
*/
|
||||
function createEmptyActionCounts(): GQLActionCounts {
|
||||
return {
|
||||
reaction: {
|
||||
total: 0,
|
||||
},
|
||||
dontAgree: {
|
||||
total: 0,
|
||||
},
|
||||
flag: {
|
||||
total: 0,
|
||||
// Note that this isn't type checked.. We force it because TS can't
|
||||
// understand the reduce.
|
||||
reasons: Object.keys(GQLCOMMENT_FLAG_REASON).reduce(
|
||||
(reasons, reason) => ({
|
||||
...reasons,
|
||||
[reason]: 0,
|
||||
}),
|
||||
{}
|
||||
) as Record<GQLCOMMENT_FLAG_REASON, number>,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* decodeActionCounts will take the encoded action counts and decode them into
|
||||
* a useable format.
|
||||
*
|
||||
* @param encodedActionCounts the action counts to decode
|
||||
*/
|
||||
export function decodeActionCounts(
|
||||
encodedActionCounts: EncodedActionCounts
|
||||
): GQLActionCounts {
|
||||
// Default all the action counts to zero.
|
||||
const actionCounts: GQLActionCounts = createEmptyActionCounts();
|
||||
|
||||
// Loop over all the encoded action counts to extract each of the action
|
||||
// counts as they are encoded.
|
||||
Object.entries(encodedActionCounts).map(([key, count]) => {
|
||||
// Pull out the action type and the reason from the key.
|
||||
const { actionType, reason } = decodeActionCountKey(key);
|
||||
|
||||
// Handle the different types and reasons.
|
||||
incrementActionCounts(actionCounts, actionType, reason, count);
|
||||
});
|
||||
|
||||
return actionCounts;
|
||||
}
|
||||
|
||||
function incrementActionCounts(
|
||||
actionCounts: GQLActionCounts,
|
||||
actionType: ACTION_TYPE,
|
||||
reason: GQLCOMMENT_FLAG_REASON | undefined,
|
||||
count: number = 1
|
||||
) {
|
||||
switch (actionType) {
|
||||
case ACTION_TYPE.REACTION:
|
||||
actionCounts.reaction.total += count;
|
||||
break;
|
||||
case ACTION_TYPE.DONT_AGREE:
|
||||
actionCounts.dontAgree.total += count;
|
||||
break;
|
||||
case ACTION_TYPE.FLAG:
|
||||
// When we have a reason, we are incrementing for that particular reason
|
||||
// rather than incrementing for the total. If we don't have a reason, we
|
||||
// just got the updated reason.
|
||||
if (reason) {
|
||||
actionCounts.flag.reasons[reason] += count;
|
||||
} else {
|
||||
actionCounts.flag.total += count;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error("unexpected action type");
|
||||
}
|
||||
|
||||
return actionCounts;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import {
|
||||
GQLACTION_GROUP,
|
||||
GQLACTION_ITEM_TYPE,
|
||||
GQLACTION_TYPE,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
export type ActionCounts = Record<string, number>;
|
||||
|
||||
export interface Action {
|
||||
readonly id: string;
|
||||
action_type: GQLACTION_TYPE;
|
||||
item_type: GQLACTION_ITEM_TYPE;
|
||||
item_id: string;
|
||||
group_id?: GQLACTION_GROUP;
|
||||
user_id?: string;
|
||||
created_at: Date;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import uuid from "uuid";
|
||||
import { Omit } from "talk-common/types";
|
||||
import { dotize } from "talk-common/utils/dotize";
|
||||
import { GQLCOMMENT_STATUS } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { EncodedActionCounts } from "talk-server/models/action";
|
||||
import { ModerationSettings } from "talk-server/models/settings";
|
||||
import { TenantResource } from "talk-server/models/tenant";
|
||||
|
||||
@@ -37,6 +38,11 @@ export interface Asset extends TenantResource {
|
||||
author?: string;
|
||||
publication_date?: Date;
|
||||
|
||||
/**
|
||||
* action_counts stores all the action counts for all Comment's on this Asset.
|
||||
*/
|
||||
action_counts: EncodedActionCounts;
|
||||
|
||||
/**
|
||||
* comment_counts stores the different counts for each comment on the Asset
|
||||
* according to their statuses.
|
||||
@@ -72,6 +78,7 @@ export async function upsertAsset(
|
||||
url,
|
||||
tenant_id: tenantID,
|
||||
created_at: now,
|
||||
action_counts: {},
|
||||
comment_counts: createEmptyCommentCounts(),
|
||||
},
|
||||
};
|
||||
@@ -250,3 +257,31 @@ export async function updateAsset(
|
||||
|
||||
return result.value || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* updateAssetActionCounts will update the given comment's action counts on
|
||||
* the Asset.
|
||||
*
|
||||
* @param mongo the database handle
|
||||
* @param tenantID the id of the Tenant
|
||||
* @param id the id of the Asset being updated
|
||||
* @param actionCounts the action counts to merge into the Asset
|
||||
*/
|
||||
export async function updateAssetActionCounts(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
id: string,
|
||||
actionCounts: EncodedActionCounts
|
||||
) {
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{ id, tenant_id: tenantID },
|
||||
// Update all the specific action counts that are associated with each of
|
||||
// the counts.
|
||||
{ $inc: dotize({ action_counts: actionCounts }) },
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
{ returnOriginal: false }
|
||||
);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
GQLCOMMENT_SORT,
|
||||
GQLCOMMENT_STATUS,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { ActionCounts } from "talk-server/models/actions";
|
||||
import { EncodedActionCounts } from "talk-server/models/action";
|
||||
import {
|
||||
Connection,
|
||||
createConnection,
|
||||
@@ -43,7 +43,7 @@ export interface Comment extends TenantResource {
|
||||
body_history: BodyHistoryItem[];
|
||||
status: GQLCOMMENT_STATUS;
|
||||
status_history: StatusHistoryItem[];
|
||||
action_counts: ActionCounts;
|
||||
action_counts: EncodedActionCounts;
|
||||
grandparent_ids: string[];
|
||||
reply_ids: string[];
|
||||
reply_count: number;
|
||||
@@ -514,10 +514,37 @@ function applyInputToQuery(input: ConnectionInput, query: Query<Comment>) {
|
||||
}
|
||||
break;
|
||||
case GQLCOMMENT_SORT.RESPECT_DESC:
|
||||
query.orderBy({ "action_counts.respect": -1, created_at: -1 });
|
||||
query.orderBy({ "action_counts.REACTION": -1, created_at: -1 });
|
||||
if (input.after) {
|
||||
query.after(input.after as number);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* updateCommentActionCounts will update the given comment's action counts.
|
||||
*
|
||||
* @param mongo the database handle
|
||||
* @param tenantID the id of the Tenant
|
||||
* @param id the id of the Comment being updated
|
||||
* @param actionCounts the action counts to merge into the Comment
|
||||
*/
|
||||
export async function updateCommentActionCounts(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
id: string,
|
||||
actionCounts: EncodedActionCounts
|
||||
) {
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{ id, tenant_id: tenantID },
|
||||
// Update all the specific action counts that are associated with each of
|
||||
// the counts.
|
||||
{ $inc: dotize({ action_counts: actionCounts }) },
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
{ returnOriginal: false }
|
||||
);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
GQLExternalIntegrations,
|
||||
GQLKarma,
|
||||
GQLMODERATION_MODE,
|
||||
GQLReactionConfiguration,
|
||||
GQLWordlist,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
@@ -101,4 +102,9 @@ export interface Settings extends ModerationSettings {
|
||||
* Various integrations with external services.
|
||||
*/
|
||||
integrations: GQLExternalIntegrations;
|
||||
|
||||
/**
|
||||
* reaction specifies the configuration for reactions.
|
||||
*/
|
||||
reaction: GQLReactionConfiguration;
|
||||
}
|
||||
|
||||
@@ -49,10 +49,10 @@ export type CreateTenantInput = Pick<
|
||||
/**
|
||||
* create will create a new Tenant.
|
||||
*
|
||||
* @param db the MongoDB connection used to create the tenant.
|
||||
* @param mongo the MongoDB connection used to create the tenant.
|
||||
* @param input the customizable parts of the Tenant available during creation
|
||||
*/
|
||||
export async function createTenant(db: Db, input: CreateTenantInput) {
|
||||
export async function createTenant(mongo: Db, input: CreateTenantInput) {
|
||||
const defaults: Sub<Tenant, CreateTenantInput> = {
|
||||
// Create a new ID.
|
||||
id: uuid.v4(),
|
||||
@@ -117,6 +117,13 @@ export async function createTenant(db: Db, input: CreateTenantInput) {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
reaction: {
|
||||
// By default, the standard reaction style will use the Respect with the
|
||||
// handshake.
|
||||
label: "Respect",
|
||||
labelActive: "Respected",
|
||||
icon: "thumb_up",
|
||||
},
|
||||
};
|
||||
|
||||
// Create the new Tenant by merging it together with the defaults.
|
||||
@@ -126,7 +133,7 @@ export async function createTenant(db: Db, input: CreateTenantInput) {
|
||||
};
|
||||
|
||||
// Insert the Tenant into the database.
|
||||
await collection(db).insert(tenant);
|
||||
await collection(mongo).insert(tenant);
|
||||
|
||||
return tenant;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
GQLUSER_ROLE,
|
||||
GQLUSER_USERNAME_STATUS,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { ActionCounts } from "talk-server/models/actions";
|
||||
import { EncodedActionCounts } from "talk-server/models/action";
|
||||
import { FilterQuery } from "talk-server/models/query";
|
||||
import { TenantResource } from "talk-server/models/tenant";
|
||||
|
||||
@@ -70,7 +70,7 @@ export interface User extends TenantResource {
|
||||
tokens: Token[];
|
||||
role: GQLUSER_ROLE;
|
||||
status: UserStatus;
|
||||
action_counts: ActionCounts;
|
||||
action_counts: EncodedActionCounts;
|
||||
ignored_users: string[];
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { GQLCOMMENT_FLAG_REPORTED_REASON } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
ACTION_ITEM_TYPE,
|
||||
ACTION_TYPE,
|
||||
CreateActionInput,
|
||||
createActions,
|
||||
deleteAction,
|
||||
DeleteActionInput,
|
||||
encodeActionCounts,
|
||||
invertEncodedActionCounts,
|
||||
} from "talk-server/models/action";
|
||||
import { updateAssetActionCounts } from "talk-server/models/asset";
|
||||
import {
|
||||
retrieveComment,
|
||||
updateCommentActionCounts,
|
||||
} from "talk-server/models/comment";
|
||||
import { Comment } from "talk-server/models/comment";
|
||||
import { Tenant } from "talk-server/models/tenant";
|
||||
import { User } from "talk-server/models/user";
|
||||
|
||||
export async function addCommentActions(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
comment: Readonly<Comment>,
|
||||
inputs: CreateActionInput[]
|
||||
): Promise<Readonly<Comment>> {
|
||||
// Create each of the actions, returning each of the action results.
|
||||
const results = await createActions(mongo, tenant.id, inputs);
|
||||
|
||||
// Get the actions that were upserted, we only want to increment the action
|
||||
// counts of actions that were just created.
|
||||
const upsertedActions = results
|
||||
.filter(({ wasUpserted }) => wasUpserted)
|
||||
.map(({ action }) => action);
|
||||
|
||||
if (upsertedActions.length > 0) {
|
||||
// Compute the action counts.
|
||||
const actionCounts = encodeActionCounts(...upsertedActions);
|
||||
|
||||
// Update the comment action counts here.
|
||||
const updatedComment = await updateCommentActionCounts(
|
||||
mongo,
|
||||
tenant.id,
|
||||
comment.id,
|
||||
actionCounts
|
||||
);
|
||||
|
||||
// Update the Asset with the updated action counts.
|
||||
await updateAssetActionCounts(
|
||||
mongo,
|
||||
tenant.id,
|
||||
comment.asset_id,
|
||||
actionCounts
|
||||
);
|
||||
|
||||
// Check to see if there was an actual comment returned (there should
|
||||
// have been, we just created it!).
|
||||
if (!updatedComment) {
|
||||
// TODO: (wyattjoh) return a better error.
|
||||
throw new Error("could not update comment action counts");
|
||||
}
|
||||
|
||||
return updatedComment;
|
||||
}
|
||||
|
||||
return comment;
|
||||
}
|
||||
|
||||
async function addCommentAction(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
input: CreateActionInput
|
||||
): Promise<Readonly<Comment>> {
|
||||
const comment = await retrieveComment(mongo, tenant.id, input.item_id);
|
||||
if (!comment) {
|
||||
// TODO: replace to match error returned by the models/comments.ts
|
||||
throw new Error("comment not found");
|
||||
}
|
||||
|
||||
return addCommentActions(mongo, tenant, comment, [input]);
|
||||
}
|
||||
|
||||
export async function removeCommentAction(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
input: DeleteActionInput
|
||||
): Promise<Readonly<Comment>> {
|
||||
// Get the Comment that we are leaving the Action on.
|
||||
const comment = await retrieveComment(mongo, tenant.id, input.item_id);
|
||||
if (!comment) {
|
||||
// TODO: replace to match error returned by the models/comments.ts
|
||||
throw new Error("comment not found");
|
||||
}
|
||||
|
||||
// Create each of the actions, returning each of the action results.
|
||||
const { wasDeleted, action } = await deleteAction(mongo, tenant.id, input);
|
||||
if (wasDeleted) {
|
||||
// Compute the action counts, and invert them (because we're deleting an
|
||||
// action).
|
||||
const actionCounts = invertEncodedActionCounts(encodeActionCounts(action!));
|
||||
|
||||
// Update the comment action counts here.
|
||||
const updatedComment = await updateCommentActionCounts(
|
||||
mongo,
|
||||
tenant.id,
|
||||
comment.id,
|
||||
actionCounts
|
||||
);
|
||||
|
||||
// Update the Asset with the updated action counts.
|
||||
await updateAssetActionCounts(
|
||||
mongo,
|
||||
tenant.id,
|
||||
comment.asset_id,
|
||||
actionCounts
|
||||
);
|
||||
|
||||
// Check to see if there was an actual comment returned.
|
||||
if (!updatedComment) {
|
||||
// TODO: (wyattjoh) return a better error.
|
||||
throw new Error("could not update comment action counts");
|
||||
}
|
||||
|
||||
return updatedComment;
|
||||
}
|
||||
|
||||
return comment;
|
||||
}
|
||||
|
||||
export type CreateCommentReaction = Pick<CreateActionInput, "item_id">;
|
||||
|
||||
export async function createReaction(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
author: User,
|
||||
input: CreateCommentReaction
|
||||
) {
|
||||
return addCommentAction(mongo, tenant, {
|
||||
action_type: ACTION_TYPE.REACTION,
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
item_id: input.item_id,
|
||||
user_id: author.id,
|
||||
});
|
||||
}
|
||||
|
||||
export type DeleteCommentReaction = Pick<DeleteActionInput, "item_id">;
|
||||
|
||||
export async function deleteReaction(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
author: User,
|
||||
input: DeleteCommentReaction
|
||||
) {
|
||||
return removeCommentAction(mongo, tenant, {
|
||||
action_type: ACTION_TYPE.REACTION,
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
item_id: input.item_id,
|
||||
user_id: author.id,
|
||||
});
|
||||
}
|
||||
|
||||
export type CreateCommentDontAgree = Pick<CreateActionInput, "item_id">;
|
||||
|
||||
export async function createDontAgree(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
author: User,
|
||||
input: CreateCommentDontAgree
|
||||
) {
|
||||
return addCommentAction(mongo, tenant, {
|
||||
action_type: ACTION_TYPE.DONT_AGREE,
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
item_id: input.item_id,
|
||||
user_id: author.id,
|
||||
});
|
||||
}
|
||||
|
||||
export type DeleteCommentDontAgree = Pick<DeleteActionInput, "item_id">;
|
||||
|
||||
export async function deleteDontAgree(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
author: User,
|
||||
input: DeleteCommentDontAgree
|
||||
) {
|
||||
return removeCommentAction(mongo, tenant, {
|
||||
action_type: ACTION_TYPE.DONT_AGREE,
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
item_id: input.item_id,
|
||||
user_id: author.id,
|
||||
});
|
||||
}
|
||||
|
||||
export type CreateCommentFlag = Pick<CreateActionInput, "item_id"> & {
|
||||
reason: GQLCOMMENT_FLAG_REPORTED_REASON;
|
||||
};
|
||||
|
||||
export async function createFlag(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
author: User,
|
||||
input: CreateCommentFlag
|
||||
) {
|
||||
return addCommentAction(mongo, tenant, {
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: input.reason,
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
item_id: input.item_id,
|
||||
user_id: author.id,
|
||||
});
|
||||
}
|
||||
|
||||
export type DeleteCommentFlag = Pick<DeleteActionInput, "item_id">;
|
||||
|
||||
export async function deleteFlag(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
author: User,
|
||||
input: DeleteCommentFlag
|
||||
) {
|
||||
return removeCommentAction(mongo, tenant, {
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
item_id: input.item_id,
|
||||
user_id: author.id,
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { Omit } from "talk-common/types";
|
||||
import { ACTION_ITEM_TYPE, CreateActionInput } from "talk-server/models/action";
|
||||
import {
|
||||
retrieveAsset,
|
||||
updateCommentStatusCount,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
} from "talk-server/models/comment";
|
||||
import { Tenant } from "talk-server/models/tenant";
|
||||
import { User } from "talk-server/models/user";
|
||||
import { addCommentActions } from "talk-server/services/comments/actions";
|
||||
import { processForModeration } from "talk-server/services/comments/moderation";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
@@ -59,7 +61,7 @@ export async function create(
|
||||
}
|
||||
|
||||
// Run the comment through the moderation phases.
|
||||
const { status, metadata } = await processForModeration({
|
||||
const { actions, status, metadata } = await processForModeration({
|
||||
asset,
|
||||
tenant,
|
||||
comment: input,
|
||||
@@ -67,9 +69,8 @@ export async function create(
|
||||
req,
|
||||
});
|
||||
|
||||
// TODO: (wyattjoh) use the actions somehow.
|
||||
|
||||
const comment = await createComment(mongo, tenant.id, {
|
||||
// Create the comment!
|
||||
let comment = await createComment(mongo, tenant.id, {
|
||||
...input,
|
||||
status,
|
||||
action_counts: {},
|
||||
@@ -77,6 +78,23 @@ export async function create(
|
||||
metadata,
|
||||
});
|
||||
|
||||
if (actions.length > 0) {
|
||||
// The actions coming from the moderation phases didn't know the item_id
|
||||
// at the time, and we didn't want the repetitive nature of adding the
|
||||
// item_type each time, so this mapping function adds them!
|
||||
const inputs = actions.map(
|
||||
(action): CreateActionInput => ({
|
||||
...action,
|
||||
item_id: comment.id,
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
})
|
||||
);
|
||||
|
||||
// Insert and handle creating the actions.
|
||||
comment = await addCommentActions(mongo, tenant, comment, inputs);
|
||||
// asse
|
||||
}
|
||||
|
||||
if (input.parent_id) {
|
||||
// Push the child's ID onto the parent.
|
||||
await pushChildCommentIDOntoParent(
|
||||
@@ -122,7 +140,7 @@ export async function edit(
|
||||
}
|
||||
|
||||
// Run the comment through the moderation phases.
|
||||
const { status, metadata } = await processForModeration({
|
||||
const { status, metadata, actions } = await processForModeration({
|
||||
asset,
|
||||
tenant,
|
||||
comment: input,
|
||||
@@ -130,9 +148,7 @@ export async function edit(
|
||||
req,
|
||||
});
|
||||
|
||||
// TODO: (wyattjoh) use the actions somehow.
|
||||
|
||||
const editedComment = await editComment(mongo, tenant.id, {
|
||||
let editedComment = await editComment(mongo, tenant.id, {
|
||||
id: input.id,
|
||||
author_id: author.id,
|
||||
body: input.body,
|
||||
@@ -146,6 +162,28 @@ export async function edit(
|
||||
Date.now() - tenant.editCommentWindowLength
|
||||
),
|
||||
});
|
||||
if (!comment) {
|
||||
// TODO: replace to match error returned by the models/comments.ts
|
||||
throw new Error("comment not found");
|
||||
}
|
||||
|
||||
if (actions.length > 0) {
|
||||
// The actions coming from the moderation phases didn't know the item_id
|
||||
// at the time, and we didn't want the repetitive nature of adding the
|
||||
// item_type each time, so this mapping function adds them!
|
||||
const inputs = actions.map(
|
||||
(action): CreateActionInput => ({
|
||||
...action,
|
||||
// Strict null check seems to have failed here... Null checking was done
|
||||
// above where we errored if the comment was falsely.
|
||||
item_id: comment!.id,
|
||||
item_type: ACTION_ITEM_TYPE.COMMENTS,
|
||||
})
|
||||
);
|
||||
|
||||
// Insert and handle creating the actions.
|
||||
editedComment = await addCommentActions(mongo, tenant, comment, inputs);
|
||||
}
|
||||
|
||||
if (comment.status !== editedComment.status) {
|
||||
// Increment the status count for the particular status on the Asset, and
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
GQLACTION_GROUP,
|
||||
GQLACTION_TYPE,
|
||||
GQLCOMMENT_FLAG_REASON,
|
||||
GQLCOMMENT_STATUS,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { ACTION_TYPE } from "talk-server/models/action";
|
||||
import {
|
||||
compose,
|
||||
ModerationPhaseContext,
|
||||
@@ -51,12 +51,12 @@ describe("compose", () => {
|
||||
|
||||
const flags = [
|
||||
{
|
||||
action_type: GQLACTION_TYPE.FLAG,
|
||||
group_id: GQLACTION_GROUP.TOXIC_COMMENT,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_TOXIC,
|
||||
},
|
||||
{
|
||||
action_type: GQLACTION_TYPE.FLAG,
|
||||
group_id: GQLACTION_GROUP.SPAM_COMMENT,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_SPAM,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -71,8 +71,8 @@ describe("compose", () => {
|
||||
() => ({
|
||||
actions: [
|
||||
{
|
||||
action_type: GQLACTION_TYPE.FLAG,
|
||||
group_id: GQLACTION_GROUP.LINKS,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_LINKS,
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -85,8 +85,8 @@ describe("compose", () => {
|
||||
}
|
||||
|
||||
expect(final.actions).not.toContainEqual({
|
||||
action_type: GQLACTION_TYPE.FLAG,
|
||||
group_id: GQLACTION_GROUP.LINKS,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_LINKS,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Omit, Promiseable } from "talk-common/types";
|
||||
import { GQLCOMMENT_STATUS } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Action } from "talk-server/models/actions";
|
||||
import { CreateActionInput } from "talk-server/models/action";
|
||||
import { Asset } from "talk-server/models/asset";
|
||||
import { Comment } from "talk-server/models/comment";
|
||||
import { Tenant } from "talk-server/models/tenant";
|
||||
@@ -9,14 +9,10 @@ import { Request } from "talk-server/types/express";
|
||||
|
||||
import { moderationPhases } from "./phases";
|
||||
|
||||
// TODO: (wyattjoh) move into actions module once we have action methods.
|
||||
export type CreateAction = Omit<
|
||||
Action,
|
||||
"id" | "item_type" | "item_id" | "created_at"
|
||||
>;
|
||||
export type ModerationAction = Omit<CreateActionInput, "item_id" | "item_type">;
|
||||
|
||||
export interface PhaseResult {
|
||||
actions: CreateAction[];
|
||||
actions: ModerationAction[];
|
||||
status: GQLCOMMENT_STATUS;
|
||||
metadata: Record<string, any>;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import striptags from "striptags";
|
||||
|
||||
import { isNil } from "lodash";
|
||||
import {
|
||||
GQLACTION_GROUP,
|
||||
GQLACTION_TYPE,
|
||||
GQLCOMMENT_FLAG_REASON,
|
||||
GQLCOMMENT_STATUS,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { ACTION_TYPE } from "talk-server/models/action";
|
||||
import { ModerationSettings } from "talk-server/models/settings";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
@@ -50,8 +50,8 @@ export const commentLength: IntermediateModerationPhase = ({
|
||||
status: GQLCOMMENT_STATUS.REJECTED,
|
||||
actions: [
|
||||
{
|
||||
action_type: GQLACTION_TYPE.FLAG,
|
||||
group_id: GQLACTION_GROUP.BODY_COUNT,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BODY_COUNT,
|
||||
metadata: {
|
||||
count: length,
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
GQLACTION_GROUP,
|
||||
GQLACTION_TYPE,
|
||||
GQLCOMMENT_FLAG_REASON,
|
||||
GQLCOMMENT_STATUS,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { ACTION_TYPE } from "talk-server/models/action";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
IntermediatePhaseResult,
|
||||
@@ -33,8 +33,8 @@ export const karma: IntermediateModerationPhase = ({
|
||||
status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD,
|
||||
actions: [
|
||||
{
|
||||
action_type: GQLACTION_TYPE.FLAG,
|
||||
group_id: GQLACTION_GROUP.TRUST,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_TOXIC,
|
||||
metadata: {
|
||||
trust: getCommentTrustScore(author),
|
||||
},
|
||||
|
||||
@@ -2,10 +2,10 @@ import linkify from "linkify-it";
|
||||
import tlds from "tlds";
|
||||
|
||||
import {
|
||||
GQLACTION_GROUP,
|
||||
GQLACTION_TYPE,
|
||||
GQLCOMMENT_FLAG_REASON,
|
||||
GQLCOMMENT_STATUS,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { ACTION_TYPE } from "talk-server/models/action";
|
||||
import { ModerationSettings } from "talk-server/models/settings";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
@@ -39,8 +39,8 @@ export const links: IntermediateModerationPhase = ({
|
||||
status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD,
|
||||
actions: [
|
||||
{
|
||||
action_type: GQLACTION_TYPE.FLAG,
|
||||
group_id: GQLACTION_GROUP.LINKS,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_LINKS,
|
||||
metadata: {
|
||||
links: comment.body,
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Client } from "akismet-api";
|
||||
|
||||
import {
|
||||
GQLACTION_GROUP,
|
||||
GQLACTION_TYPE,
|
||||
GQLCOMMENT_FLAG_REASON,
|
||||
GQLCOMMENT_STATUS,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import logger from "talk-server/logger";
|
||||
import { ACTION_TYPE } from "talk-server/models/action";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
IntermediatePhaseResult,
|
||||
@@ -110,8 +110,8 @@ export const spam: IntermediateModerationPhase = async ({
|
||||
status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD,
|
||||
actions: [
|
||||
{
|
||||
action_type: GQLACTION_TYPE.FLAG,
|
||||
group_id: GQLACTION_GROUP.SPAM_COMMENT,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_SPAM,
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
|
||||
@@ -4,12 +4,12 @@ import fetch from "node-fetch";
|
||||
|
||||
import { Omit } from "talk-common/types";
|
||||
import {
|
||||
GQLACTION_GROUP,
|
||||
GQLACTION_TYPE,
|
||||
GQLCOMMENT_FLAG_REASON,
|
||||
GQLCOMMENT_STATUS,
|
||||
GQLPerspectiveExternalIntegration,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import logger from "talk-server/logger";
|
||||
import { ACTION_TYPE } from "talk-server/models/action";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
IntermediatePhaseResult,
|
||||
@@ -103,8 +103,8 @@ export const toxic: IntermediateModerationPhase = async ({
|
||||
status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD,
|
||||
actions: [
|
||||
{
|
||||
action_type: GQLACTION_TYPE.FLAG,
|
||||
group_id: GQLACTION_GROUP.TOXIC_COMMENT,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_TOXIC,
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
GQLACTION_GROUP,
|
||||
GQLACTION_TYPE,
|
||||
GQLCOMMENT_FLAG_REASON,
|
||||
GQLCOMMENT_STATUS,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { ACTION_TYPE } from "talk-server/models/action";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
IntermediatePhaseResult,
|
||||
@@ -29,8 +29,8 @@ export const wordlist: IntermediateModerationPhase = ({
|
||||
status: GQLCOMMENT_STATUS.REJECTED,
|
||||
actions: [
|
||||
{
|
||||
action_type: GQLACTION_TYPE.FLAG,
|
||||
group_id: GQLACTION_GROUP.BANNED_WORD,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BANNED_WORD,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -46,8 +46,8 @@ export const wordlist: IntermediateModerationPhase = ({
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
action_type: GQLACTION_TYPE.FLAG,
|
||||
group_id: GQLACTION_GROUP.SUSPECT_WORD,
|
||||
action_type: ACTION_TYPE.FLAG,
|
||||
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_SUSPECT_WORD,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user