From 4107fd367f6ebf39953c8d5220397863bf6a096c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Sat, 22 Sep 2018 00:55:55 +0200 Subject: [PATCH 01/20] Implement LocalReplyListContainer --- src/core/client/stream/local/local.graphql | 1 + .../tabs/comments/components/ReplyList.tsx | 6 +- .../containers/LocalReplyListContainer.tsx | 63 +++++++++++++++++++ .../containers/ReplyListContainer.tsx | 10 ++- 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 src/core/client/stream/tabs/comments/containers/LocalReplyListContainer.tsx diff --git a/src/core/client/stream/local/local.graphql b/src/core/client/stream/local/local.graphql index 62e52d4e8..6c01257c5 100644 --- a/src/core/client/stream/local/local.graphql +++ b/src/core/client/stream/local/local.graphql @@ -21,6 +21,7 @@ type AuthPopup { extend type Comment { pending: Boolean + localReplies: CommentsConnection } type Local { diff --git a/src/core/client/stream/tabs/comments/components/ReplyList.tsx b/src/core/client/stream/tabs/comments/components/ReplyList.tsx index e942ae5a9..735c49600 100644 --- a/src/core/client/stream/tabs/comments/components/ReplyList.tsx +++ b/src/core/client/stream/tabs/comments/components/ReplyList.tsx @@ -17,9 +17,9 @@ export interface ReplyListProps { comments: ReadonlyArray< { id: string } & PropTypesOf["comment"] >; - onShowAll: () => void; - hasMore: boolean; - disableShowAll: boolean; + onShowAll?: () => void; + hasMore?: boolean; + disableShowAll?: boolean; indentLevel?: number; ReplyListComponent?: React.ComponentType; } diff --git a/src/core/client/stream/tabs/comments/containers/LocalReplyListContainer.tsx b/src/core/client/stream/tabs/comments/containers/LocalReplyListContainer.tsx new file mode 100644 index 000000000..ddafe99a7 --- /dev/null +++ b/src/core/client/stream/tabs/comments/containers/LocalReplyListContainer.tsx @@ -0,0 +1,63 @@ +import React, { Component } from "react"; +import { graphql } from "react-relay"; + +import withFragmentContainer from "talk-framework/lib/relay/withFragmentContainer"; +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 ReplyList from "../components/ReplyList"; + +interface InnerProps { + indentLevel: number; + me: MeData; + asset: AssetData; + comment: CommentData; +} + +export class LocalReplyListContainer extends Component { + public render() { + if (!this.props.comment.localReplies) { + return null; + } + return ( + e.node)} + asset={this.props.asset} + indentLevel={this.props.indentLevel} + /> + ); + } +} + +const enhanced = withFragmentContainer({ + me: graphql` + fragment LocalReplyListContainer_me on User { + ...CommentContainer_me + } + `, + asset: graphql` + fragment LocalReplyListContainer_asset on Asset { + ...CommentContainer_asset + } + `, + comment: graphql` + fragment LocalReplyListContainer_comment on Comment { + id + localReplies { + edges { + node { + id + ...CommentContainer_comment + } + } + } + } + `, +})(LocalReplyListContainer); + +export type LocalReplyListContainerProps = PropTypesOf; +export default enhanced; diff --git a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx index f9744f55a..c707cdc94 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx @@ -3,6 +3,7 @@ import { graphql, GraphQLTaggedNode, RelayPaginationProp } from "react-relay"; import { withProps } from "recompose"; import { withPaginationContainer } from "talk-framework/lib/relay"; +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"; @@ -12,6 +13,7 @@ import { } from "talk-stream/__generated__/ReplyListContainer1PaginationQuery.graphql"; import ReplyList from "../components/ReplyList"; +import LocalReplyListContainer from "./LocalReplyListContainer"; export interface InnerProps { me: MeData | null; @@ -121,11 +123,13 @@ const ReplyListContainer5 = createReplyListContainer( me: graphql` fragment ReplyListContainer5_me on User { ...CommentContainer_me + ...LocalReplyListContainer_me } `, asset: graphql` fragment ReplyListContainer5_asset on Asset { ...CommentContainer_asset + ...LocalReplyListContainer_asset } `, comment: graphql` @@ -142,6 +146,7 @@ const ReplyListContainer5 = createReplyListContainer( node { id ...CommentContainer_comment + ...LocalReplyListContainer_comment } } } @@ -162,7 +167,10 @@ const ReplyListContainer5 = createReplyListContainer( @arguments(count: $count, cursor: $cursor, orderBy: $orderBy) } } - ` + `, + (props: PropTypesOf) => ( + + ) ); const ReplyListContainer4 = createReplyListContainer( From 936347e8e4bc54f675607a7f8e657086bc78bc15 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 24 Sep 2018 11:36:44 -0600 Subject: [PATCH 02/20] fix: restricted action counts on Asset's --- src/core/server/graph/tenant/schema/schema.graphql | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index c3cdf71b4..2f275ca0f 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -931,9 +931,10 @@ type Asset { ): CommentsConnection! """ - actionCounts stores the counts of all the actions for the Comment. + actionCounts stores the counts of all the actions against this Asset and it's + Comments. """ - actionCounts: ActionCounts! + actionCounts: ActionCounts! @auth(roles: [ADMIN, MODERATOR]) """ author is the authors listed in the meta tags for the Asset. From 62a1fc590149d6dcc0632afdef6cdc692ecb72c0 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 24 Sep 2018 20:08:11 +0200 Subject: [PATCH 03/20] Implement local reply list for last threading level --- src/core/client/stream/local/local.graphql | 2 +- .../stream/mutations/CreateCommentMutation.ts | 123 +- .../tabs/comments/components/Indent.css | 6 + .../tabs/comments/components/Indent.tsx | 1 + .../tabs/comments/components/ReplyList.tsx | 4 + .../comments/containers/CommentContainer.tsx | 23 +- .../containers/LocalReplyListContainer.tsx | 11 +- .../ReplyCommentFormContainer.spec.tsx | 2 +- .../containers/ReplyCommentFormContainer.tsx | 2 + .../containers/ReplyListContainer.spec.tsx | 3 + .../containers/ReplyListContainer.tsx | 10 +- .../ReplyListContainer.spec.tsx.snap | 4 + .../postLocalReply.spec.tsx.snap | 2939 +++++++++++++++++ .../test/comments/postLocalReply.spec.tsx | 110 + src/core/client/stream/test/fixtures.ts | 107 + 15 files changed, 3292 insertions(+), 55 deletions(-) create mode 100644 src/core/client/stream/test/comments/__snapshots__/postLocalReply.spec.tsx.snap create mode 100644 src/core/client/stream/test/comments/postLocalReply.spec.tsx diff --git a/src/core/client/stream/local/local.graphql b/src/core/client/stream/local/local.graphql index 6c01257c5..088e86390 100644 --- a/src/core/client/stream/local/local.graphql +++ b/src/core/client/stream/local/local.graphql @@ -21,7 +21,7 @@ type AuthPopup { extend type Comment { pending: Boolean - localReplies: CommentsConnection + localReplies: [Comment!] } type Local { diff --git a/src/core/client/stream/mutations/CreateCommentMutation.ts b/src/core/client/stream/mutations/CreateCommentMutation.ts index b858f04e2..b2f1b78fe 100644 --- a/src/core/client/stream/mutations/CreateCommentMutation.ts +++ b/src/core/client/stream/mutations/CreateCommentMutation.ts @@ -1,5 +1,9 @@ import { graphql } from "react-relay"; -import { Environment, RelayMutationConfig } from "relay-runtime"; +import { + ConnectionHandler, + Environment, + RecordSourceSelectorProxy, +} from "relay-runtime"; import { getMe } from "talk-framework/helpers"; import { TalkContext } from "talk-framework/lib/bootstrap"; @@ -14,7 +18,79 @@ import { CreateCommentMutation as MutationTypes } from "talk-stream/__generated_ export type CreateCommentInput = Omit< MutationTypes["variables"]["input"], "clientMutationId" ->; +> & { local?: boolean }; + +function sharedUpdater( + store: RecordSourceSelectorProxy, + input: CreateCommentInput +) { + if (input.local) { + localUpdate(store, input); + } else { + update(store, input); + } +} + +function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) { + // Get the payload returned from the server. + const payload = store.getRootField("createComment")!; + + // Get the edge of the newly created comment. + const newEdge = payload.getLinkedRecord("edge")!; + + // Get parent proxy. + const parentProxy = input.parentID + ? store.get(input.parentID) + : store.get(input.assetID); + + const connectionKey = input.parentID + ? "ReplyList_replies" + : "Stream_comments"; + + const filters = input.parentID + ? { orderBy: "CREATED_AT_ASC" } + : { orderBy: "CREATED_AT_DESC" }; + + const where = input.parentID ? "append" : "prepend"; + + if (parentProxy) { + const con = ConnectionHandler.getConnection( + parentProxy, + connectionKey, + filters + ); + if (con) { + if (where === "prepend") { + ConnectionHandler.insertEdgeBefore(con, newEdge); + } else { + ConnectionHandler.insertEdgeAfter(con, newEdge); + } + } + } +} + +function localUpdate( + store: RecordSourceSelectorProxy, + input: CreateCommentInput +) { + // Get the payload returned from the server. + const payload = store.getRootField("createComment")!; + + // Get the edge of the newly created comment. + const newEdge = payload.getLinkedRecord("edge")!; + const newComment = newEdge.getLinkedRecord("node"); + + // Get parent proxy. + const parentProxy = store.get(input.parentID!); + + if (parentProxy) { + const localReplies = parentProxy.getLinkedRecords("localReplies"); + const nextLocalReplies = localReplies + ? localReplies.concat(newComment) + : [newComment]; + parentProxy.setLinkedRecords(nextLocalReplies, "localReplies"); + } +} const mutation = graphql` mutation CreateCommentMutation($input: CreateCommentInput!) { @@ -32,39 +108,6 @@ const mutation = graphql` let clientMutationId = 0; -function getConfig(input: CreateCommentInput): RelayMutationConfig[] { - if (!input.parentID) { - return [ - { - type: "RANGE_ADD", - connectionInfo: [ - { - key: "Stream_comments", - rangeBehavior: "prepend", - filters: { orderBy: "CREATED_AT_DESC" }, - }, - ], - parentID: input.assetID, - edgeName: "edge", - }, - ]; - } - return [ - { - type: "RANGE_ADD", - connectionInfo: [ - { - key: "ReplyList_replies", - rangeBehavior: "append", - filters: { orderBy: "CREATED_AT_ASC" }, - }, - ], - parentID: input.parentID, - edgeName: "edge", - }, - ]; -} - function commit( environment: Environment, input: CreateCommentInput, @@ -77,7 +120,9 @@ function commit( mutation, variables: { input: { - ...input, + assetID: input.assetID, + parentID: input.parentID, + body: input.body, clientMutationId: clientMutationId.toString(), }, }, @@ -102,9 +147,13 @@ function commit( }, } as any, // TODO: (cvle) generated types should contain one for the optimistic response. optimisticUpdater: store => { + sharedUpdater(store, input); store.get(id)!.setValue(true, "pending"); }, - configs: getConfig(input), + updater: store => { + sharedUpdater(store, input); + }, + // configs: getConfig(input), }); } diff --git a/src/core/client/stream/tabs/comments/components/Indent.css b/src/core/client/stream/tabs/comments/components/Indent.css index b88e0b642..87f598224 100644 --- a/src/core/client/stream/tabs/comments/components/Indent.css +++ b/src/core/client/stream/tabs/comments/components/Indent.css @@ -30,6 +30,12 @@ border-left: 3px solid var(--palette-grey-lighter); } +.level6 { + padding-left: var(--spacing-unit); + margin-left: calc(5 * var(--spacing-unit)); + border-left: 3px solid var(--palette-grey-lightest); +} + .noBorder { border: 0; } diff --git a/src/core/client/stream/tabs/comments/components/Indent.tsx b/src/core/client/stream/tabs/comments/components/Indent.tsx index e91cdb974..45bac72a5 100644 --- a/src/core/client/stream/tabs/comments/components/Indent.tsx +++ b/src/core/client/stream/tabs/comments/components/Indent.tsx @@ -20,6 +20,7 @@ const Indent: StatelessComponent = props => { [styles.level3]: props.level === 3, [styles.level4]: props.level === 4, [styles.level5]: props.level === 5, + [styles.level6]: props.level === 6, [styles.noBorder]: props.noBorder, })} > diff --git a/src/core/client/stream/tabs/comments/components/ReplyList.tsx b/src/core/client/stream/tabs/comments/components/ReplyList.tsx index 735c49600..896120b91 100644 --- a/src/core/client/stream/tabs/comments/components/ReplyList.tsx +++ b/src/core/client/stream/tabs/comments/components/ReplyList.tsx @@ -22,6 +22,8 @@ export interface ReplyListProps { disableShowAll?: boolean; indentLevel?: number; ReplyListComponent?: React.ComponentType; + localReply?: boolean; + disableReplies?: boolean; } function getReplyListElement( @@ -48,6 +50,8 @@ const ReplyList: StatelessComponent = props => { comment={comment} asset={props.asset} indentLevel={props.indentLevel} + localReply={props.localReply} + disableReplies={props.disableReplies} /> {getReplyListElement(props, comment)} diff --git a/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx b/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx index 24e09fd23..bfeca8995 100644 --- a/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx @@ -26,6 +26,8 @@ interface InnerProps { asset: AssetData; indentLevel?: number; showAuthPopup: ShowAuthPopupMutation; + localReply?: boolean; + disableReplies?: boolean; } interface State { @@ -107,7 +109,13 @@ export class CommentContainer extends Component { } public render() { - const { comment, asset, indentLevel } = this.props; + const { + comment, + asset, + indentLevel, + localReply, + disableReplies, + } = this.props; const { showReplyDialog, showEditDialog, editable } = this.state; if (showEditDialog) { return ( @@ -144,11 +152,13 @@ export class CommentContainer extends Component { } footer={ <> - + {!disableReplies && ( + + )} } @@ -158,6 +168,7 @@ export class CommentContainer extends Component { comment={comment} asset={asset} onClose={this.closeReplyDialog} + localReply={localReply} /> )} diff --git a/src/core/client/stream/tabs/comments/containers/LocalReplyListContainer.tsx b/src/core/client/stream/tabs/comments/containers/LocalReplyListContainer.tsx index ddafe99a7..2429536cb 100644 --- a/src/core/client/stream/tabs/comments/containers/LocalReplyListContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/LocalReplyListContainer.tsx @@ -25,9 +25,10 @@ export class LocalReplyListContainer extends Component { e.node)} + comments={this.props.comment.localReplies} asset={this.props.asset} indentLevel={this.props.indentLevel} + disableReplies /> ); } @@ -48,12 +49,8 @@ const enhanced = withFragmentContainer({ fragment LocalReplyListContainer_comment on Comment { id localReplies { - edges { - node { - id - ...CommentContainer_comment - } - } + id + ...CommentContainer_comment } } `, diff --git a/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.spec.tsx b/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.spec.tsx index e189de00d..25a90d517 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.spec.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.spec.tsx @@ -92,7 +92,7 @@ it("save values", async () => { it("creates a comment", async () => { const assetID = "asset-id"; - const input = { body: "Hello World!" }; + const input = { body: "Hello World!", local: false }; const createCommentStub = sinon.stub(); const form = { reset: noop }; const onCloseStub = sinon.stub(); diff --git a/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.tsx b/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.tsx index 6a724b82e..a634d2776 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.tsx @@ -25,6 +25,7 @@ interface InnerProps { asset: AssetData; onClose?: () => void; autofocus: boolean; + localReply?: boolean; } interface State { @@ -76,6 +77,7 @@ export class ReplyCommentFormContainer extends Component { await this.props.createComment({ assetID: this.props.asset.id, parentID: this.props.comment.id, + local: this.props.localReply, ...input, }); diff --git a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.spec.tsx b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.spec.tsx index 08508b157..d383a4e7e 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.spec.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.spec.tsx @@ -29,6 +29,7 @@ it("renders correctly", () => { me: null, indentLevel: 1, ReplyListComponent: () => null, + localReply: false, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); @@ -50,6 +51,7 @@ it("renders correctly when replies are null", () => { me: null, indentLevel: 1, ReplyListComponent: undefined, + localReply: false, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); @@ -75,6 +77,7 @@ describe("when has more replies", () => { me: null, indentLevel: 1, ReplyListComponent: undefined, + localReply: false, }; let wrapper: ShallowWrapper; diff --git a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx index c707cdc94..9edf46d1e 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx @@ -22,6 +22,7 @@ export interface InnerProps { relay: RelayPaginationProp; indentLevel: number; ReplyListComponent: React.ComponentType | undefined; + localReply: boolean | undefined; } // TODO: (cvle) This should be autogenerated. @@ -54,6 +55,7 @@ export class ReplyListContainer extends React.Component { disableShowAll={this.state.disableShowAll} indentLevel={this.props.indentLevel} ReplyListComponent={this.props.ReplyListComponent} + localReply={this.props.localReply} /> ); } @@ -85,9 +87,10 @@ function createReplyListContainer( comment: GraphQLTaggedNode; }, query: GraphQLTaggedNode, - ReplyListComponent?: React.ComponentType + ReplyListComponent?: React.ComponentType, + localReply?: boolean ) { - return withProps({ indentLevel, ReplyListComponent })( + return withProps({ indentLevel, ReplyListComponent, localReply })( withPaginationContainer< InnerProps, ReplyListContainer1PaginationQueryVariables, @@ -170,7 +173,8 @@ const ReplyListContainer5 = createReplyListContainer( `, (props: PropTypesOf) => ( - ) + ), + true ); const ReplyListContainer4 = createReplyListContainer( diff --git a/src/core/client/stream/tabs/comments/containers/__snapshots__/ReplyListContainer.spec.tsx.snap b/src/core/client/stream/tabs/comments/containers/__snapshots__/ReplyListContainer.spec.tsx.snap index ad14ab002..d98e6786a 100644 --- a/src/core/client/stream/tabs/comments/containers/__snapshots__/ReplyListContainer.spec.tsx.snap +++ b/src/core/client/stream/tabs/comments/containers/__snapshots__/ReplyListContainer.spec.tsx.snap @@ -39,6 +39,7 @@ exports[`renders correctly 1`] = ` } disableShowAll={false} indentLevel={1} + localReply={false} me={null} onShowAll={[Function]} /> @@ -85,6 +86,7 @@ exports[`when has more replies renders hasMore 1`] = ` disableShowAll={false} hasMore={true} indentLevel={1} + localReply={false} me={null} onShowAll={[Function]} /> @@ -129,6 +131,7 @@ exports[`when has more replies when showing all disables show all button 1`] = ` disableShowAll={true} hasMore={true} indentLevel={1} + localReply={false} me={null} onShowAll={[Function]} /> @@ -173,6 +176,7 @@ exports[`when has more replies when showing all enable show all button after loa disableShowAll={false} hasMore={true} indentLevel={1} + localReply={false} me={null} onShowAll={[Function]} /> diff --git a/src/core/client/stream/test/comments/__snapshots__/postLocalReply.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/postLocalReply.spec.tsx.snap new file mode 100644 index 000000000..35948074c --- /dev/null +++ b/src/core/client/stream/test/comments/__snapshots__/postLocalReply.spec.tsx.snap @@ -0,0 +1,2939 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`post a reply: open reply form 1`] = ` +
+
+
+
+
+
+ Signed in as + + Markus + + . +
+
+ + Not you?  + + +
+
+
+
+
+
+ +
+
+
+ + + +
+ +
+
+
+
+
+
+ + Powered by + + ⁨The Coral Project⁩ + + +
+ +
+
+ +
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+ + + +
+ +
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`post a reply: optimistic response 1`] = ` +
+
+
+
+
+
+ Signed in as + + Markus + + . +
+
+ + Not you?  + + +
+
+
+
+
+
+ +
+
+
+ + + +
+ +
+
+
+
+
+
+ + Powered by + + ⁨The Coral Project⁩ + + +
+ +
+
+ +
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+ + + +
+
Hello world!", + } + } + disabled={true} + id="comments-replyCommentForm-rte-comment-with-deepest-replies-5" + onBlur={[Function]} + onChange={[Function]} + onCut={[Function]} + onFocus={[Function]} + onInput={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onSelect={[Function]} + /> +
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+ +
+
+
Hello world!", + } + } + /> +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`post a reply: server response 1`] = ` +
+
+
+
+
+
+ Signed in as + + Markus + + . +
+
+ + Not you?  + + +
+
+
+
+
+
+ +
+
+
+ + + +
+ +
+
+
+
+
+
+ + Powered by + + ⁨The Coral Project⁩ + + +
+ +
+
+ +
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
Hello world! (from server)", + } + } + /> +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`renders comment stream 1`] = ` +
+
+
+
+
+
+ Signed in as + + Markus + + . +
+
+ + Not you?  + + +
+
+
+
+
+
+ +
+
+
+ + + +
+ +
+
+
+
+
+
+ + Powered by + + ⁨The Coral Project⁩ + + +
+ +
+
+ +
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/core/client/stream/test/comments/postLocalReply.spec.tsx b/src/core/client/stream/test/comments/postLocalReply.spec.tsx new file mode 100644 index 000000000..f0e5c8949 --- /dev/null +++ b/src/core/client/stream/test/comments/postLocalReply.spec.tsx @@ -0,0 +1,110 @@ +import { ReactTestRenderer } from "react-test-renderer"; +import timekeeper from "timekeeper"; + +import { timeout } from "talk-common/utils"; +import { createSinonStub } from "talk-framework/testHelpers"; + +import { assetWithDeepestReplies, users } from "../fixtures"; +import create from "./create"; + +let testRenderer: ReactTestRenderer; +beforeEach(() => { + const resolvers = { + Query: { + asset: createSinonStub( + s => s.throws(), + s => s.returns(assetWithDeepestReplies) + ), + me: createSinonStub(s => s.throws(), s => s.returns(users[0])), + }, + Mutation: { + createComment: createSinonStub( + s => s.throws(), + s => + s + .withArgs(undefined, { + input: { + assetID: assetWithDeepestReplies.id, + parentID: "comment-with-deepest-replies-5", + body: "Hello world!", + clientMutationId: "0", + }, + }) + .returns({ + edge: { + cursor: null, + node: { + id: "comment-x", + author: users[0], + body: "Hello world! (from server)", + 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", + }) + ), + }, + }; + + ({ testRenderer } = create({ + // Set this to true, to see graphql responses. + logNetwork: false, + resolvers, + initLocalState: localRecord => { + localRecord.setValue(assetWithDeepestReplies.id, "assetID"); + }, + })); +}); + +it("renders comment stream", async () => { + // Wait for loading. + await timeout(); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("post a reply", async () => { + // Wait for loading. + await timeout(); + + // Open reply form. + testRenderer.root + .findByProps({ + id: + "comments-commentContainer-replyButton-comment-with-deepest-replies-5", + }) + .props.onClick(); + + await timeout(); + expect(testRenderer.toJSON()).toMatchSnapshot("open reply form"); + + // Write reply . + testRenderer.root + .findByProps({ + inputId: "comments-replyCommentForm-rte-comment-with-deepest-replies-5", + }) + .props.onChange({ html: "Hello world!" }); + + timekeeper.freeze(new Date("2018-07-06T18:24:00.000Z")); + testRenderer.root + .findByProps({ + id: "comments-replyCommentForm-form-comment-with-deepest-replies-5", + }) + .props.onSubmit(); + // Test optimistic response. + expect(testRenderer.toJSON()).toMatchSnapshot("optimistic response"); + timekeeper.reset(); + + // Wait for loading. + await timeout(); + + // Test after server response. + expect(testRenderer.toJSON()).toMatchSnapshot("server response"); +}); diff --git a/src/core/client/stream/test/fixtures.ts b/src/core/client/stream/test/fixtures.ts index 47485cfa7..a0e6e6a31 100644 --- a/src/core/client/stream/test/fixtures.ts +++ b/src/core/client/stream/test/fixtures.ts @@ -171,3 +171,110 @@ export const assetWithDeepReplies = { }, }, }; + +export const commentWithDeepestReplies = { + ...commentWithReplies, + id: "comment-with-deepest-replies", + body: "body 0", + replies: { + ...commentWithReplies.replies, + edges: [ + { + cursor: commentWithReplies.createdAt, + node: { + ...commentWithReplies, + id: "comment-with-deepest-replies-1", + body: "body 1", + replies: { + ...commentWithReplies.replies, + edges: [ + { + cursor: commentWithReplies.createdAt, + node: { + ...commentWithReplies, + id: "comment-with-deepest-replies-2", + body: "body 2", + replies: { + ...commentWithReplies.replies, + edges: [ + { + cursor: commentWithReplies.createdAt, + node: { + ...commentWithReplies, + id: "comment-with-deepest-replies-3", + body: "body 3", + replies: { + ...commentWithReplies.replies, + edges: [ + { + cursor: commentWithReplies.createdAt, + node: { + ...commentWithReplies, + id: "comment-with-deepest-replies-4", + body: "body 4", + replies: { + ...commentWithReplies.replies, + edges: [ + { + cursor: commentWithReplies.createdAt, + node: { + ...commentWithReplies, + id: "comment-with-deepest-replies-5", + body: "body 5", + replies: { + ...commentWithReplies.replies, + edges: [ + { + cursor: + commentWithReplies.createdAt, + node: { + ...commentWithReplies, + id: + "comment-with-deepest-replies-6", + body: "body 6", + replies: { + ...commentWithReplies.replies, + edges: [], + }, + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + ], + }, +}; + +export const assetWithDeepestReplies = { + id: "asset-with-deepest-replies", + url: "http://localhost/assets/asset-with-replies", + isClosed: false, + comments: { + edges: [ + { + node: commentWithDeepestReplies, + cursor: commentWithDeepestReplies.createdAt, + }, + ], + pageInfo: { + hasNextPage: false, + }, + }, +}; From 6e743b9b75da2cd1449edab2bad14efa2cecc0c8 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 24 Sep 2018 20:13:54 +0200 Subject: [PATCH 04/20] More tests --- .../comments/components/ReplyList.spec.tsx | 2 ++ .../__snapshots__/ReplyList.spec.tsx.snap | 4 +++ .../containers/CommentContainer.spec.tsx | 32 +++++++++++++++++++ .../CommentContainer.spec.tsx.snap | 26 +++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/src/core/client/stream/tabs/comments/components/ReplyList.spec.tsx b/src/core/client/stream/tabs/comments/components/ReplyList.spec.tsx index b978b0955..c8b3dcbb2 100644 --- a/src/core/client/stream/tabs/comments/components/ReplyList.spec.tsx +++ b/src/core/client/stream/tabs/comments/components/ReplyList.spec.tsx @@ -20,6 +20,8 @@ it("renders correctly", () => { disableShowAll: false, indentLevel: 1, me: null, + localReply: false, + disableReplies: false, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); diff --git a/src/core/client/stream/tabs/comments/components/__snapshots__/ReplyList.spec.tsx.snap b/src/core/client/stream/tabs/comments/components/__snapshots__/ReplyList.spec.tsx.snap index cd78852c3..0b4cbcd15 100644 --- a/src/core/client/stream/tabs/comments/components/__snapshots__/ReplyList.spec.tsx.snap +++ b/src/core/client/stream/tabs/comments/components/__snapshots__/ReplyList.spec.tsx.snap @@ -19,8 +19,10 @@ exports[`renders correctly 1`] = ` "id": "comment-1", } } + disableReplies={false} indentLevel={1} key="comment-1" + localReply={false} me={null} /> @@ -38,8 +40,10 @@ exports[`renders correctly 1`] = ` "id": "comment-2", } } + disableReplies={false} indentLevel={1} key="comment-2" + localReply={false} me={null} /> diff --git a/src/core/client/stream/tabs/comments/containers/CommentContainer.spec.tsx b/src/core/client/stream/tabs/comments/containers/CommentContainer.spec.tsx index 772a8df79..049062795 100644 --- a/src/core/client/stream/tabs/comments/containers/CommentContainer.spec.tsx +++ b/src/core/client/stream/tabs/comments/containers/CommentContainer.spec.tsx @@ -32,6 +32,8 @@ it("renders username and body", () => { }, indentLevel: 1, showAuthPopup: noop as any, + localReply: false, + disableReplies: false, }; const wrapper = shallow(); @@ -65,3 +67,33 @@ it("renders body only", () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); + +it("hide reply button", () => { + const props: PropTypesOf = { + me: null, + asset: { + id: "asset-id", + }, + comment: { + id: "comment-id", + author: { + id: "author-id", + username: "Marvin", + }, + body: "Woof", + createdAt: "1995-12-17T03:24:00.000Z", + editing: { + edited: false, + editableUntil: "1995-12-17T03:24:30.000Z", + }, + pending: false, + }, + indentLevel: 1, + showAuthPopup: noop as any, + localReply: false, + disableReplies: true, + }; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/tabs/comments/containers/__snapshots__/CommentContainer.spec.tsx.snap b/src/core/client/stream/tabs/comments/containers/__snapshots__/CommentContainer.spec.tsx.snap index db1219282..1f37c5456 100644 --- a/src/core/client/stream/tabs/comments/containers/__snapshots__/CommentContainer.spec.tsx.snap +++ b/src/core/client/stream/tabs/comments/containers/__snapshots__/CommentContainer.spec.tsx.snap @@ -1,5 +1,31 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`hide reply button 1`] = ` + + + + + } + id="comment-comment-id" + indentLevel={1} + showEditedMarker={false} + /> + +`; + exports[`renders body only 1`] = ` Date: Mon, 24 Sep 2018 20:16:29 +0200 Subject: [PATCH 05/20] Add some comments --- src/core/client/stream/mutations/CreateCommentMutation.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/client/stream/mutations/CreateCommentMutation.ts b/src/core/client/stream/mutations/CreateCommentMutation.ts index b2f1b78fe..c20406746 100644 --- a/src/core/client/stream/mutations/CreateCommentMutation.ts +++ b/src/core/client/stream/mutations/CreateCommentMutation.ts @@ -31,6 +31,9 @@ function sharedUpdater( } } +/** + * update integrates new comment into the CommentConnection. + */ function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) { // Get the payload returned from the server. const payload = store.getRootField("createComment")!; @@ -69,6 +72,9 @@ function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) { } } +/** + * localUpdate is like update but updates the `localReplies` endpoint. + */ function localUpdate( store: RecordSourceSelectorProxy, input: CreateCommentInput From dc3ea366bc46a1d075e0ceb9e2ff3d02b9fc85f0 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 24 Sep 2018 20:24:24 +0200 Subject: [PATCH 06/20] More comments --- src/core/client/stream/local/local.graphql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/client/stream/local/local.graphql b/src/core/client/stream/local/local.graphql index 088e86390..45887aff2 100644 --- a/src/core/client/stream/local/local.graphql +++ b/src/core/client/stream/local/local.graphql @@ -20,7 +20,10 @@ type AuthPopup { } extend type Comment { + # pending is true during the optimistic response. pending: Boolean + # localReplies contains only comments created by the user + # on the ultimate threading level. localReplies: [Comment!] } From 8c3b0071c2a2d43e1b644d82b0c9a773caf26e31 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 24 Sep 2018 12:32:57 -0600 Subject: [PATCH 07/20] fix: renamed, middleware fixes --- src/core/server/app/middleware/error.ts | 10 ++-------- src/core/server/app/middleware/playground.ts | 15 ++++++++++++++- src/core/server/graph/tenant/loaders/comments.ts | 2 +- src/core/server/graph/tenant/resolvers/asset.ts | 2 +- src/core/server/graph/tenant/resolvers/comment.ts | 2 +- .../models/{actions.spec.ts => action.spec.ts} | 2 +- src/core/server/models/{actions.ts => action.ts} | 1 + src/core/server/models/asset.ts | 2 +- src/core/server/models/comment.ts | 2 +- src/core/server/models/user.ts | 2 +- src/core/server/services/comments/index.ts | 2 +- .../services/comments/moderation/index.spec.ts | 2 +- .../server/services/comments/moderation/index.ts | 2 +- .../comments/moderation/phases/commentLength.ts | 2 +- .../services/comments/moderation/phases/karma.ts | 2 +- .../services/comments/moderation/phases/links.ts | 2 +- .../services/comments/moderation/phases/spam.ts | 2 +- .../services/comments/moderation/phases/toxic.ts | 2 +- .../comments/moderation/phases/wordlist.ts | 2 +- 19 files changed, 33 insertions(+), 25 deletions(-) rename src/core/server/models/{actions.spec.ts => action.spec.ts} (99%) rename src/core/server/models/{actions.ts => action.ts} (99%) diff --git a/src/core/server/app/middleware/error.ts b/src/core/server/app/middleware/error.ts index b7be782cb..8eadfc187 100644 --- a/src/core/server/app/middleware/error.ts +++ b/src/core/server/app/middleware/error.ts @@ -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); } }; diff --git a/src/core/server/app/middleware/playground.ts b/src/core/server/app/middleware/playground.ts index 1873a0537..0364d10aa 100644 --- a/src/core/server/app/middleware/playground.ts +++ b/src/core/server/app/middleware/playground.ts @@ -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); + } +}; diff --git a/src/core/server/graph/tenant/loaders/comments.ts b/src/core/server/graph/tenant/loaders/comments.ts index c90c1ef6b..abd78803f 100644 --- a/src/core/server/graph/tenant/loaders/comments.ts +++ b/src/core/server/graph/tenant/loaders/comments.ts @@ -10,7 +10,7 @@ import { import { ACTION_ITEM_TYPE, retrieveManyUserActionPresence, -} from "talk-server/models/actions"; +} from "talk-server/models/action"; import { retrieveCommentAssetConnection, retrieveCommentRepliesConnection, diff --git a/src/core/server/graph/tenant/resolvers/asset.ts b/src/core/server/graph/tenant/resolvers/asset.ts index 98c09ca88..f1ebc640c 100644 --- a/src/core/server/graph/tenant/resolvers/asset.ts +++ b/src/core/server/graph/tenant/resolvers/asset.ts @@ -1,5 +1,5 @@ import { GQLAssetTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; -import { decodeActionCounts } from "talk-server/models/actions"; +import { decodeActionCounts } from "talk-server/models/action"; import { Asset } from "talk-server/models/asset"; const Asset: GQLAssetTypeResolver = { diff --git a/src/core/server/graph/tenant/resolvers/comment.ts b/src/core/server/graph/tenant/resolvers/comment.ts index 25b9b2f73..5919732d0 100644 --- a/src/core/server/graph/tenant/resolvers/comment.ts +++ b/src/core/server/graph/tenant/resolvers/comment.ts @@ -1,5 +1,5 @@ import { GQLCommentTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; -import { decodeActionCounts } from "talk-server/models/actions"; +import { decodeActionCounts } from "talk-server/models/action"; import { Comment } from "talk-server/models/comment"; const Comment: GQLCommentTypeResolver = { diff --git a/src/core/server/models/actions.spec.ts b/src/core/server/models/action.spec.ts similarity index 99% rename from src/core/server/models/actions.spec.ts rename to src/core/server/models/action.spec.ts index a09595003..f89f587eb 100644 --- a/src/core/server/models/actions.spec.ts +++ b/src/core/server/models/action.spec.ts @@ -6,7 +6,7 @@ import { decodeActionCounts, encodeActionCounts, validateAction, -} from "talk-server/models/actions"; +} from "talk-server/models/action"; describe("#encodeActionCounts", () => { it("generates the action counts correctly", () => { diff --git a/src/core/server/models/actions.ts b/src/core/server/models/action.ts similarity index 99% rename from src/core/server/models/actions.ts rename to src/core/server/models/action.ts index a9e3a4654..2c1834c4e 100644 --- a/src/core/server/models/actions.ts +++ b/src/core/server/models/action.ts @@ -186,6 +186,7 @@ export async function createActions( tenantID: string, inputs: CreateActionInput[] ): Promise { + // TODO: (wyattjoh) replace with a batch write. return Promise.all(inputs.map(input => createAction(mongo, tenantID, input))); } diff --git a/src/core/server/models/asset.ts b/src/core/server/models/asset.ts index 0d15cf6d3..aa3289503 100644 --- a/src/core/server/models/asset.ts +++ b/src/core/server/models/asset.ts @@ -3,7 +3,7 @@ import uuid from "uuid"; import { Omit } from "talk-common/types"; import { dotize } from "talk-common/utils/dotize"; -import { EncodedActionCounts } from "talk-server/models/actions"; +import { EncodedActionCounts } from "talk-server/models/action"; import { ModerationSettings } from "talk-server/models/settings"; import { TenantResource } from "talk-server/models/tenant"; diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index 99780bda1..e2ff8da1d 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -7,7 +7,7 @@ import { GQLCOMMENT_SORT, GQLCOMMENT_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; -import { EncodedActionCounts } from "talk-server/models/actions"; +import { EncodedActionCounts } from "talk-server/models/action"; import { Connection, Cursor, diff --git a/src/core/server/models/user.ts b/src/core/server/models/user.ts index 8bfac404a..a89d3b42a 100644 --- a/src/core/server/models/user.ts +++ b/src/core/server/models/user.ts @@ -7,7 +7,7 @@ import { GQLUSER_ROLE, GQLUSER_USERNAME_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; -import { EncodedActionCounts } 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"; diff --git a/src/core/server/services/comments/index.ts b/src/core/server/services/comments/index.ts index 7fa33fa75..7acc5f5bb 100644 --- a/src/core/server/services/comments/index.ts +++ b/src/core/server/services/comments/index.ts @@ -6,7 +6,7 @@ import { CreateActionInput, createActions, encodeActionCounts, -} from "talk-server/models/actions"; +} from "talk-server/models/action"; import { retrieveAsset, updateAssetActionCounts, diff --git a/src/core/server/services/comments/moderation/index.spec.ts b/src/core/server/services/comments/moderation/index.spec.ts index f0367bb65..3adcccfd2 100644 --- a/src/core/server/services/comments/moderation/index.spec.ts +++ b/src/core/server/services/comments/moderation/index.spec.ts @@ -2,7 +2,7 @@ import { GQLCOMMENT_FLAG_REASON, GQLCOMMENT_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; -import { ACTION_TYPE } from "talk-server/models/actions"; +import { ACTION_TYPE } from "talk-server/models/action"; import { compose, ModerationPhaseContext, diff --git a/src/core/server/services/comments/moderation/index.ts b/src/core/server/services/comments/moderation/index.ts index cc4d91a34..7bf497f75 100644 --- a/src/core/server/services/comments/moderation/index.ts +++ b/src/core/server/services/comments/moderation/index.ts @@ -1,6 +1,6 @@ import { Omit, Promiseable } from "talk-common/types"; import { GQLCOMMENT_STATUS } from "talk-server/graph/tenant/schema/__generated__/types"; -import { CreateActionInput } 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"; diff --git a/src/core/server/services/comments/moderation/phases/commentLength.ts b/src/core/server/services/comments/moderation/phases/commentLength.ts index 65e37df03..5ea6393fd 100644 --- a/src/core/server/services/comments/moderation/phases/commentLength.ts +++ b/src/core/server/services/comments/moderation/phases/commentLength.ts @@ -2,7 +2,7 @@ import { GQLCOMMENT_FLAG_REASON, GQLCOMMENT_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; -import { ACTION_TYPE } from "talk-server/models/actions"; +import { ACTION_TYPE } from "talk-server/models/action"; import { ModerationSettings } from "talk-server/models/settings"; import { IntermediateModerationPhase, diff --git a/src/core/server/services/comments/moderation/phases/karma.ts b/src/core/server/services/comments/moderation/phases/karma.ts index 4fdf50fec..91ffb069f 100755 --- a/src/core/server/services/comments/moderation/phases/karma.ts +++ b/src/core/server/services/comments/moderation/phases/karma.ts @@ -2,7 +2,7 @@ import { GQLCOMMENT_FLAG_REASON, GQLCOMMENT_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; -import { ACTION_TYPE } from "talk-server/models/actions"; +import { ACTION_TYPE } from "talk-server/models/action"; import { IntermediateModerationPhase, IntermediatePhaseResult, diff --git a/src/core/server/services/comments/moderation/phases/links.ts b/src/core/server/services/comments/moderation/phases/links.ts index 4bf584428..ae2c0e723 100755 --- a/src/core/server/services/comments/moderation/phases/links.ts +++ b/src/core/server/services/comments/moderation/phases/links.ts @@ -5,7 +5,7 @@ import { GQLCOMMENT_FLAG_REASON, GQLCOMMENT_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; -import { ACTION_TYPE } from "talk-server/models/actions"; +import { ACTION_TYPE } from "talk-server/models/action"; import { ModerationSettings } from "talk-server/models/settings"; import { IntermediateModerationPhase, diff --git a/src/core/server/services/comments/moderation/phases/spam.ts b/src/core/server/services/comments/moderation/phases/spam.ts index 2e73b90dc..5e3c495a7 100644 --- a/src/core/server/services/comments/moderation/phases/spam.ts +++ b/src/core/server/services/comments/moderation/phases/spam.ts @@ -5,7 +5,7 @@ import { GQLCOMMENT_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; import logger from "talk-server/logger"; -import { ACTION_TYPE } from "talk-server/models/actions"; +import { ACTION_TYPE } from "talk-server/models/action"; import { IntermediateModerationPhase, IntermediatePhaseResult, diff --git a/src/core/server/services/comments/moderation/phases/toxic.ts b/src/core/server/services/comments/moderation/phases/toxic.ts index 0a9cda887..ec3a9f6ae 100644 --- a/src/core/server/services/comments/moderation/phases/toxic.ts +++ b/src/core/server/services/comments/moderation/phases/toxic.ts @@ -9,7 +9,7 @@ import { GQLPerspectiveExternalIntegration, } from "talk-server/graph/tenant/schema/__generated__/types"; import logger from "talk-server/logger"; -import { ACTION_TYPE } from "talk-server/models/actions"; +import { ACTION_TYPE } from "talk-server/models/action"; import { IntermediateModerationPhase, IntermediatePhaseResult, diff --git a/src/core/server/services/comments/moderation/phases/wordlist.ts b/src/core/server/services/comments/moderation/phases/wordlist.ts index 4a4eebb42..05f6431b9 100755 --- a/src/core/server/services/comments/moderation/phases/wordlist.ts +++ b/src/core/server/services/comments/moderation/phases/wordlist.ts @@ -2,7 +2,7 @@ import { GQLCOMMENT_FLAG_REASON, GQLCOMMENT_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; -import { ACTION_TYPE } from "talk-server/models/actions"; +import { ACTION_TYPE } from "talk-server/models/action"; import { IntermediateModerationPhase, IntermediatePhaseResult, From dee14709c873511104deb99484d64ab12f00cc2a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 24 Sep 2018 12:34:27 -0600 Subject: [PATCH 08/20] feat: added reaction adding mutation --- .../server/graph/tenant/mutators/comment.ts | 11 +++++- .../server/graph/tenant/resolvers/mutation.ts | 12 ++++--- .../server/graph/tenant/schema/schema.graphql | 36 +++++++++++++++++++ src/core/server/services/comments/index.ts | 29 +++++++++++++++ 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/core/server/graph/tenant/mutators/comment.ts b/src/core/server/graph/tenant/mutators/comment.ts index aeaac70ee..8a8e86066 100644 --- a/src/core/server/graph/tenant/mutators/comment.ts +++ b/src/core/server/graph/tenant/mutators/comment.ts @@ -1,9 +1,14 @@ import TenantContext from "talk-server/graph/tenant/context"; import { GQLCreateCommentInput, + GQLCreateCommentReactionInput, GQLEditCommentInput, } from "talk-server/graph/tenant/schema/__generated__/types"; -import { create, edit } from "talk-server/services/comments"; +import { + create, + createCommentReaction, + edit, +} from "talk-server/services/comments"; export default (ctx: TenantContext) => ({ create: (input: GQLCreateCommentInput) => @@ -30,4 +35,8 @@ export default (ctx: TenantContext) => ({ }, ctx.req ), + createReaction: (input: GQLCreateCommentReactionInput) => + createCommentReaction(ctx.mongo, ctx.tenant, ctx.user!, { + item_id: input.commentID, + }), }); diff --git a/src/core/server/graph/tenant/resolvers/mutation.ts b/src/core/server/graph/tenant/resolvers/mutation.ts index 5b7c65dc4..2ff79b8d2 100644 --- a/src/core/server/graph/tenant/resolvers/mutation.ts +++ b/src/core/server/graph/tenant/resolvers/mutation.ts @@ -9,10 +9,10 @@ const Mutation: GQLMutationTypeResolver = { 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,10 @@ const Mutation: GQLMutationTypeResolver = { 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, + }), }; export default Mutation; diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 2f275ca0f..f03de4ffc 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -1478,6 +1478,34 @@ type UpdateSettingsPayload { ## Mutation ################## +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! +} + +################## +## Mutation +################## + type Mutation { """ createComment will create a Comment as the current logged in User. @@ -1495,6 +1523,14 @@ 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 } ################################################################################ diff --git a/src/core/server/services/comments/index.ts b/src/core/server/services/comments/index.ts index 7acc5f5bb..ee1fb969d 100644 --- a/src/core/server/services/comments/index.ts +++ b/src/core/server/services/comments/index.ts @@ -3,6 +3,7 @@ import { Db } from "mongodb"; import { Omit } from "talk-common/types"; import { ACTION_ITEM_TYPE, + ACTION_TYPE, CreateActionInput, createActions, encodeActionCounts, @@ -220,3 +221,31 @@ async function addCommentActions( return comment; } + +export type CreateCommentReaction = Pick; + +export async function createCommentReaction( + mongo: Db, + tenant: Tenant, + author: User, + input: CreateCommentReaction +) { + // Get the Comment that we are leaving the Action on. + let 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"); + } + + // Add the comment actions, and return the Comment that we just updated. + comment = await addCommentActions(mongo, tenant, comment, [ + { + action_type: ACTION_TYPE.REACTION, + item_type: ACTION_ITEM_TYPE.COMMENTS, + item_id: input.item_id, + user_id: author.id, + }, + ]); + + return comment; +} From 7139563ceb42adefd96a2f266b150c1892e9cc54 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 24 Sep 2018 20:36:39 +0200 Subject: [PATCH 09/20] More comments --- .../stream/tabs/comments/containers/CommentContainer.tsx | 5 +++++ .../tabs/comments/containers/LocalReplyListContainer.tsx | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx b/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx index bfeca8995..f9db4932c 100644 --- a/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx @@ -26,7 +26,12 @@ interface InnerProps { asset: AssetData; indentLevel?: number; showAuthPopup: ShowAuthPopupMutation; + /** + * localReply will integrate the mutation response into + * localReplies + */ localReply?: boolean; + /** disableReplies will remove the ReplyButton */ disableReplies?: boolean; } diff --git a/src/core/client/stream/tabs/comments/containers/LocalReplyListContainer.tsx b/src/core/client/stream/tabs/comments/containers/LocalReplyListContainer.tsx index 2429536cb..025160cf9 100644 --- a/src/core/client/stream/tabs/comments/containers/LocalReplyListContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/LocalReplyListContainer.tsx @@ -16,6 +16,12 @@ interface InnerProps { comment: CommentData; } +/** + * LocalReplyListContainer renders the replies from the endpoint + * `localReplies` instead of `replies`. This is e.g. used for the + * ultimate threading level to only display the newly created comments + * from the current user. + */ export class LocalReplyListContainer extends Component { public render() { if (!this.props.comment.localReplies) { From 526a3c634a3eb967bd0ddf2dfaad95e1252ddc63 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 24 Sep 2018 20:41:46 +0200 Subject: [PATCH 10/20] Give last level a display name --- .../tabs/comments/containers/ReplyListContainer.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx index 9edf46d1e..0dfe3a80f 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx @@ -12,6 +12,7 @@ import { ReplyListContainer1PaginationQueryVariables, } from "talk-stream/__generated__/ReplyListContainer1PaginationQuery.graphql"; +import { StatelessComponent } from "enzyme"; import ReplyList from "../components/ReplyList"; import LocalReplyListContainer from "./LocalReplyListContainer"; @@ -120,6 +121,13 @@ function createReplyListContainer( ); } +/** + * LastReplyList uses the LocalReplyListContainer. + */ +const LastReplyList: StatelessComponent< + PropTypesOf +> = props => ; + const ReplyListContainer5 = createReplyListContainer( 5, { @@ -171,9 +179,7 @@ const ReplyListContainer5 = createReplyListContainer( } } `, - (props: PropTypesOf) => ( - - ), + LastReplyList, true ); From 10748347b9293ad3f12c31eadaeaf15b3d42e1bb Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 24 Sep 2018 12:50:30 -0600 Subject: [PATCH 11/20] feat: support deleting comment reactions --- .../server/graph/tenant/mutators/comment.ts | 10 ++- .../server/graph/tenant/resolvers/mutation.ts | 4 ++ .../server/graph/tenant/schema/schema.graphql | 38 +++++++++- src/core/server/models/action.ts | 21 ++++++ src/core/server/services/comments/index.ts | 72 ++++++++++++++++++- 5 files changed, 141 insertions(+), 4 deletions(-) diff --git a/src/core/server/graph/tenant/mutators/comment.ts b/src/core/server/graph/tenant/mutators/comment.ts index 8a8e86066..ba0211d1b 100644 --- a/src/core/server/graph/tenant/mutators/comment.ts +++ b/src/core/server/graph/tenant/mutators/comment.ts @@ -2,11 +2,13 @@ import TenantContext from "talk-server/graph/tenant/context"; import { GQLCreateCommentInput, GQLCreateCommentReactionInput, + GQLDeleteCommentReactionInput, GQLEditCommentInput, } from "talk-server/graph/tenant/schema/__generated__/types"; import { create, - createCommentReaction, + createReaction, + deleteReaction, edit, } from "talk-server/services/comments"; @@ -36,7 +38,11 @@ export default (ctx: TenantContext) => ({ ctx.req ), createReaction: (input: GQLCreateCommentReactionInput) => - createCommentReaction(ctx.mongo, ctx.tenant, ctx.user!, { + 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, }), }); diff --git a/src/core/server/graph/tenant/resolvers/mutation.ts b/src/core/server/graph/tenant/resolvers/mutation.ts index 2ff79b8d2..25746017f 100644 --- a/src/core/server/graph/tenant/resolvers/mutation.ts +++ b/src/core/server/graph/tenant/resolvers/mutation.ts @@ -27,6 +27,10 @@ const Mutation: GQLMutationTypeResolver = { 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, + }), }; export default Mutation; diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index f03de4ffc..6a4b05051 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -1475,7 +1475,7 @@ type UpdateSettingsPayload { } ################## -## Mutation +## createCommentReaction ################## input CreateCommentReactionInput { @@ -1502,6 +1502,34 @@ type CreateCommentReactionPayload { 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! +} + ################## ## Mutation ################## @@ -1531,6 +1559,14 @@ type Mutation { 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 } ################################################################################ diff --git a/src/core/server/models/action.ts b/src/core/server/models/action.ts index 2c1834c4e..c5b8c36d9 100644 --- a/src/core/server/models/action.ts +++ b/src/core/server/models/action.ts @@ -315,6 +315,27 @@ export function encodeActionCounts(...actions: Action[]): EncodedActionCounts { 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`. diff --git a/src/core/server/services/comments/index.ts b/src/core/server/services/comments/index.ts index ee1fb969d..dde8826af 100644 --- a/src/core/server/services/comments/index.ts +++ b/src/core/server/services/comments/index.ts @@ -6,7 +6,10 @@ import { ACTION_TYPE, CreateActionInput, createActions, + deleteAction, + DeleteActionInput, encodeActionCounts, + invertEncodedActionCounts, } from "talk-server/models/action"; import { retrieveAsset, @@ -222,9 +225,50 @@ async function addCommentActions( return comment; } +async function deleteCommentAction( + mongo: Db, + tenant: Tenant, + comment: Readonly, + input: DeleteActionInput +): Promise> { + // 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; -export async function createCommentReaction( +export async function createReaction( mongo: Db, tenant: Tenant, author: User, @@ -249,3 +293,29 @@ export async function createCommentReaction( return comment; } + +export type DeleteCommentReaction = Pick; + +export async function deleteReaction( + mongo: Db, + tenant: Tenant, + author: User, + input: DeleteCommentReaction +) { + // Get the Comment that we are leaving the Action on. + let 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"); + } + + // Add the comment actions, and return the Comment that we just updated. + comment = await deleteCommentAction(mongo, tenant, comment, { + action_type: ACTION_TYPE.REACTION, + item_type: ACTION_ITEM_TYPE.COMMENTS, + item_id: input.item_id, + user_id: author.id, + }); + + return comment; +} From a19ac4e2ccf29025073711784d61fc05c3d2105d Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 24 Sep 2018 13:08:16 -0600 Subject: [PATCH 12/20] fix: addressed test updates --- .../__snapshots__/{actions.spec.ts.snap => action.spec.ts.snap} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/core/server/models/__snapshots__/{actions.spec.ts.snap => action.spec.ts.snap} (100%) diff --git a/src/core/server/models/__snapshots__/actions.spec.ts.snap b/src/core/server/models/__snapshots__/action.spec.ts.snap similarity index 100% rename from src/core/server/models/__snapshots__/actions.spec.ts.snap rename to src/core/server/models/__snapshots__/action.spec.ts.snap From 3cdaa284ed759706a269f2c879f860abbb7eb720 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 24 Sep 2018 13:46:13 -0600 Subject: [PATCH 13/20] feat: added flag creation and deletion mutations --- .../server/graph/tenant/mutators/comment.ts | 13 +++ .../server/graph/tenant/resolvers/mutation.ts | 8 ++ .../server/graph/tenant/schema/schema.graphql | 79 ++++++++++++++++++- src/core/server/models/action.ts | 19 ++++- src/core/server/services/comments/index.ts | 58 ++++++++++++++ 5 files changed, 173 insertions(+), 4 deletions(-) diff --git a/src/core/server/graph/tenant/mutators/comment.ts b/src/core/server/graph/tenant/mutators/comment.ts index ba0211d1b..734e724b5 100644 --- a/src/core/server/graph/tenant/mutators/comment.ts +++ b/src/core/server/graph/tenant/mutators/comment.ts @@ -1,13 +1,17 @@ import TenantContext from "talk-server/graph/tenant/context"; import { + GQLCreateCommentFlagInput, GQLCreateCommentInput, GQLCreateCommentReactionInput, + GQLDeleteCommentFlagInput, GQLDeleteCommentReactionInput, GQLEditCommentInput, } from "talk-server/graph/tenant/schema/__generated__/types"; import { create, + createFlag, createReaction, + deleteFlag, deleteReaction, edit, } from "talk-server/services/comments"; @@ -45,4 +49,13 @@ export default (ctx: TenantContext) => ({ deleteReaction(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, + }), }); diff --git a/src/core/server/graph/tenant/resolvers/mutation.ts b/src/core/server/graph/tenant/resolvers/mutation.ts index 25746017f..9074ccfc8 100644 --- a/src/core/server/graph/tenant/resolvers/mutation.ts +++ b/src/core/server/graph/tenant/resolvers/mutation.ts @@ -31,6 +31,14 @@ const Mutation: GQLMutationTypeResolver = { comment: await ctx.mutators.Comment.deleteReaction(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; diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 6a4b05051..7ca1ec4dc 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -1530,6 +1530,67 @@ type DeleteCommentReactionPayload { 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 ################## @@ -1554,7 +1615,7 @@ type Mutation { """ createCommentReaction will create a Reaction authored by the current logged in - user on a Comment. + User on a Comment. """ createCommentReaction( input: CreateCommentReactionInput! @@ -1562,11 +1623,25 @@ type Mutation { """ deleteCommentReaction will delete a Reaction authored by the current logged in - user on a Comment if it exists. + User on a Comment if it exists. """ deleteCommentReaction( input: CreateCommentReactionInput! ): CreateCommentReactionPayload @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 } ################################################################################ diff --git a/src/core/server/models/action.ts b/src/core/server/models/action.ts index c5b8c36d9..bcc1c4fd3 100644 --- a/src/core/server/models/action.ts +++ b/src/core/server/models/action.ts @@ -7,7 +7,9 @@ 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"; @@ -45,12 +47,20 @@ 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?: GQLCOMMENT_FLAG_REASON; + reason?: FLAG_REASON; user_id?: string; created_at: Date; metadata?: Record; @@ -274,10 +284,15 @@ export async function deleteAction( action_type: input.action_type, item_type: input.item_type, item_id: input.item_id, - reason: input.reason, 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 { diff --git a/src/core/server/services/comments/index.ts b/src/core/server/services/comments/index.ts index dde8826af..947ac4ae9 100644 --- a/src/core/server/services/comments/index.ts +++ b/src/core/server/services/comments/index.ts @@ -1,6 +1,7 @@ import { Db } from "mongodb"; import { Omit } from "talk-common/types"; +import { GQLCOMMENT_FLAG_REPORTED_REASON } from "talk-server/graph/tenant/schema/__generated__/types"; import { ACTION_ITEM_TYPE, ACTION_TYPE, @@ -319,3 +320,60 @@ export async function deleteReaction( return comment; } + +export type CreateCommentFlag = Pick & { + reason: GQLCOMMENT_FLAG_REPORTED_REASON; +}; + +export async function createFlag( + mongo: Db, + tenant: Tenant, + author: User, + input: CreateCommentFlag +) { + // Get the Comment that we are leaving the Action on. + let 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"); + } + + // Add the comment actions, and return the Comment that we just updated. + comment = await addCommentActions(mongo, tenant, comment, [ + { + action_type: ACTION_TYPE.FLAG, + reason: input.reason, + item_type: ACTION_ITEM_TYPE.COMMENTS, + item_id: input.item_id, + user_id: author.id, + }, + ]); + + return comment; +} + +export type DeleteCommentFlag = Pick; + +export async function deleteFlag( + mongo: Db, + tenant: Tenant, + author: User, + input: DeleteCommentFlag +) { + // Get the Comment that we are leaving the Action on. + let 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"); + } + + // Add the comment actions, and return the Comment that we just updated. + comment = await deleteCommentAction(mongo, tenant, comment, { + action_type: ACTION_TYPE.FLAG, + item_type: ACTION_ITEM_TYPE.COMMENTS, + item_id: input.item_id, + user_id: author.id, + }); + + return comment; +} From 8c2d811b55f6229be0659bbc118923616713212a Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 24 Sep 2018 22:04:03 +0200 Subject: [PATCH 14/20] Remove unused line --- src/core/client/stream/mutations/CreateCommentMutation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/client/stream/mutations/CreateCommentMutation.ts b/src/core/client/stream/mutations/CreateCommentMutation.ts index c20406746..71ea3d894 100644 --- a/src/core/client/stream/mutations/CreateCommentMutation.ts +++ b/src/core/client/stream/mutations/CreateCommentMutation.ts @@ -159,7 +159,6 @@ function commit( updater: store => { sharedUpdater(store, input); }, - // configs: getConfig(input), }); } From 3c640f87d32bfb2c5768642dbc2df53c8d5eb5c8 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 24 Sep 2018 14:07:44 -0600 Subject: [PATCH 15/20] fix: cleanup of comments service --- .../server/graph/tenant/mutators/comment.ts | 5 +- src/core/server/services/comments/actions.ts | 197 ++++++++++++++++ src/core/server/services/comments/index.ts | 220 +----------------- 3 files changed, 202 insertions(+), 220 deletions(-) create mode 100644 src/core/server/services/comments/actions.ts diff --git a/src/core/server/graph/tenant/mutators/comment.ts b/src/core/server/graph/tenant/mutators/comment.ts index 734e724b5..0867ee5e3 100644 --- a/src/core/server/graph/tenant/mutators/comment.ts +++ b/src/core/server/graph/tenant/mutators/comment.ts @@ -7,14 +7,13 @@ import { GQLDeleteCommentReactionInput, GQLEditCommentInput, } from "talk-server/graph/tenant/schema/__generated__/types"; +import { create, edit } from "talk-server/services/comments"; import { - create, createFlag, createReaction, deleteFlag, deleteReaction, - edit, -} from "talk-server/services/comments"; +} from "talk-server/services/comments/actions"; export default (ctx: TenantContext) => ({ create: (input: GQLCreateCommentInput) => diff --git a/src/core/server/services/comments/actions.ts b/src/core/server/services/comments/actions.ts new file mode 100644 index 000000000..f8313c913 --- /dev/null +++ b/src/core/server/services/comments/actions.ts @@ -0,0 +1,197 @@ +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, + inputs: CreateActionInput[] +): Promise> { + // 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> { + 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> { + // 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; + +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; + +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 CreateCommentFlag = Pick & { + 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; + +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, + }); +} diff --git a/src/core/server/services/comments/index.ts b/src/core/server/services/comments/index.ts index 947ac4ae9..6388df472 100644 --- a/src/core/server/services/comments/index.ts +++ b/src/core/server/services/comments/index.ts @@ -1,32 +1,18 @@ import { Db } from "mongodb"; import { Omit } from "talk-common/types"; -import { GQLCOMMENT_FLAG_REPORTED_REASON } from "talk-server/graph/tenant/schema/__generated__/types"; +import { ACTION_ITEM_TYPE, CreateActionInput } from "talk-server/models/action"; +import { retrieveAsset } from "talk-server/models/asset"; import { - ACTION_ITEM_TYPE, - ACTION_TYPE, - CreateActionInput, - createActions, - deleteAction, - DeleteActionInput, - encodeActionCounts, - invertEncodedActionCounts, -} from "talk-server/models/action"; -import { - retrieveAsset, - updateAssetActionCounts, -} from "talk-server/models/asset"; -import { - Comment, createComment, CreateCommentInput, editComment, EditCommentInput, retrieveComment, - updateCommentActionCounts, } 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"; @@ -177,203 +163,3 @@ export async function edit( return comment; } - -async function addCommentActions( - mongo: Db, - tenant: Tenant, - comment: Readonly, - inputs: CreateActionInput[] -): Promise> { - // 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 deleteCommentAction( - mongo: Db, - tenant: Tenant, - comment: Readonly, - input: DeleteActionInput -): Promise> { - // 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; - -export async function createReaction( - mongo: Db, - tenant: Tenant, - author: User, - input: CreateCommentReaction -) { - // Get the Comment that we are leaving the Action on. - let 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"); - } - - // Add the comment actions, and return the Comment that we just updated. - comment = await addCommentActions(mongo, tenant, comment, [ - { - action_type: ACTION_TYPE.REACTION, - item_type: ACTION_ITEM_TYPE.COMMENTS, - item_id: input.item_id, - user_id: author.id, - }, - ]); - - return comment; -} - -export type DeleteCommentReaction = Pick; - -export async function deleteReaction( - mongo: Db, - tenant: Tenant, - author: User, - input: DeleteCommentReaction -) { - // Get the Comment that we are leaving the Action on. - let 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"); - } - - // Add the comment actions, and return the Comment that we just updated. - comment = await deleteCommentAction(mongo, tenant, comment, { - action_type: ACTION_TYPE.REACTION, - item_type: ACTION_ITEM_TYPE.COMMENTS, - item_id: input.item_id, - user_id: author.id, - }); - - return comment; -} - -export type CreateCommentFlag = Pick & { - reason: GQLCOMMENT_FLAG_REPORTED_REASON; -}; - -export async function createFlag( - mongo: Db, - tenant: Tenant, - author: User, - input: CreateCommentFlag -) { - // Get the Comment that we are leaving the Action on. - let 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"); - } - - // Add the comment actions, and return the Comment that we just updated. - comment = await addCommentActions(mongo, tenant, comment, [ - { - action_type: ACTION_TYPE.FLAG, - reason: input.reason, - item_type: ACTION_ITEM_TYPE.COMMENTS, - item_id: input.item_id, - user_id: author.id, - }, - ]); - - return comment; -} - -export type DeleteCommentFlag = Pick; - -export async function deleteFlag( - mongo: Db, - tenant: Tenant, - author: User, - input: DeleteCommentFlag -) { - // Get the Comment that we are leaving the Action on. - let 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"); - } - - // Add the comment actions, and return the Comment that we just updated. - comment = await deleteCommentAction(mongo, tenant, comment, { - action_type: ACTION_TYPE.FLAG, - item_type: ACTION_ITEM_TYPE.COMMENTS, - item_id: input.item_id, - user_id: author.id, - }); - - return comment; -} From 460429d99143f1034f19187166751ee44107c743 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 24 Sep 2018 22:11:03 +0200 Subject: [PATCH 16/20] Refactor indent level className --- .../tabs/comments/components/Indent.tsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/core/client/stream/tabs/comments/components/Indent.tsx b/src/core/client/stream/tabs/comments/components/Indent.tsx index 45bac72a5..660bea468 100644 --- a/src/core/client/stream/tabs/comments/components/Indent.tsx +++ b/src/core/client/stream/tabs/comments/components/Indent.tsx @@ -10,17 +10,30 @@ export interface IndentProps { children: React.ReactNode; } +const levels = [ + styles.level1, + styles.level2, + styles.level3, + styles.level4, + styles.level5, + styles.level6, +]; + +function getLevelClassName(level?: number) { + if (!level) { + return ""; + } + if (level - 1 > levels.length) { + throw new Error(`Indent level ${level} does not exist`); + } + return levels[level - 1]; +} + const Indent: StatelessComponent = props => { return (
From 02909fb3998d4c3229ec0f83c760562f21fe69b9 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 24 Sep 2018 22:16:10 +0200 Subject: [PATCH 17/20] Simplify getLevelClassName --- .../client/stream/tabs/comments/components/Indent.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/core/client/stream/tabs/comments/components/Indent.tsx b/src/core/client/stream/tabs/comments/components/Indent.tsx index 660bea468..81a418d63 100644 --- a/src/core/client/stream/tabs/comments/components/Indent.tsx +++ b/src/core/client/stream/tabs/comments/components/Indent.tsx @@ -11,6 +11,7 @@ export interface IndentProps { } const levels = [ + "", styles.level1, styles.level2, styles.level3, @@ -19,14 +20,11 @@ const levels = [ styles.level6, ]; -function getLevelClassName(level?: number) { - if (!level) { - return ""; - } - if (level - 1 > levels.length) { +function getLevelClassName(level: number = 0) { + if (!(level in levels)) { throw new Error(`Indent level ${level} does not exist`); } - return levels[level - 1]; + return levels[level]; } const Indent: StatelessComponent = props => { From 7cce84b7f82611c3a63abcfd712c73bdc9bfe737 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 24 Sep 2018 14:25:48 -0600 Subject: [PATCH 18/20] feat: added dontAgree create/delete mutations --- .../server/graph/tenant/mutators/comment.ts | 12 ++++ .../server/graph/tenant/resolvers/mutation.ts | 8 +++ .../server/graph/tenant/schema/schema.graphql | 72 +++++++++++++++++++ src/core/server/services/comments/actions.ts | 32 +++++++++ 4 files changed, 124 insertions(+) diff --git a/src/core/server/graph/tenant/mutators/comment.ts b/src/core/server/graph/tenant/mutators/comment.ts index 0867ee5e3..01c734185 100644 --- a/src/core/server/graph/tenant/mutators/comment.ts +++ b/src/core/server/graph/tenant/mutators/comment.ts @@ -1,16 +1,20 @@ 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"; @@ -48,6 +52,14 @@ export default (ctx: TenantContext) => ({ 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, diff --git a/src/core/server/graph/tenant/resolvers/mutation.ts b/src/core/server/graph/tenant/resolvers/mutation.ts index 9074ccfc8..77f0ca109 100644 --- a/src/core/server/graph/tenant/resolvers/mutation.ts +++ b/src/core/server/graph/tenant/resolvers/mutation.ts @@ -31,6 +31,14 @@ const Mutation: GQLMutationTypeResolver = { 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, diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 7ca1ec4dc..c96f32305 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -1530,6 +1530,62 @@ type DeleteCommentReactionPayload { 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 ################## @@ -1629,6 +1685,22 @@ type Mutation { 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. diff --git a/src/core/server/services/comments/actions.ts b/src/core/server/services/comments/actions.ts index f8313c913..e788757c9 100644 --- a/src/core/server/services/comments/actions.ts +++ b/src/core/server/services/comments/actions.ts @@ -161,6 +161,38 @@ export async function deleteReaction( }); } +export type CreateCommentDontAgree = Pick; + +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; + +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 & { reason: GQLCOMMENT_FLAG_REPORTED_REASON; }; From a318b56233f80829d2d41ac804c8cd466bc14543 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 24 Sep 2018 15:38:12 -0600 Subject: [PATCH 19/20] fix: patch for sort bug --- src/core/server/models/comment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index e2ff8da1d..39d8ccc5c 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -366,7 +366,7 @@ function applyInputToQuery(input: ConnectionInput, query: Query) { } 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); } From 04b4b7e715ada0360204e377b0f0fc72cccf322f Mon Sep 17 00:00:00 2001 From: Kiwi Date: Tue, 25 Sep 2018 18:25:42 +0200 Subject: [PATCH 20/20] Upgrade rte (#1906) --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index efc1a3a5c..6e669dc80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1342,9 +1342,9 @@ } }, "@coralproject/rte": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@coralproject/rte/-/rte-0.10.11.tgz", - "integrity": "sha512-Tq8NznCOYx84QpHhZbUldxcYrztEQnnNjDC2aW5HhzltO9nBG2Hu78MzTwliML3MmaTFbLehdIb/BKGKj4eMSw==", + "version": "0.10.12", + "resolved": "https://registry.npmjs.org/@coralproject/rte/-/rte-0.10.12.tgz", + "integrity": "sha512-w7UWe6u+TNoPtFcWvyjYkV7eaAE4ccTOYrhdWPsDfM34ZDRq+bM5eiiAMcUVHizvHgsUzFWoN2qjOo+jSfIjCw==", "dev": true, "requires": { "bowser": "^1.0.0", diff --git a/package.json b/package.json index 9a804811f..7e6f14e73 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "@babel/polyfill": "7.0.0-beta.49", "@babel/preset-env": "7.0.0-beta.49", "@babel/preset-react": "7.0.0-beta.49", - "@coralproject/rte": "^0.10.11", + "@coralproject/rte": "^0.10.12", "@types/bcryptjs": "^2.4.1", "@types/bull": "^3.3.16", "@types/bunyan": "^1.8.4",