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/22] 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 0fbd27cdcb3eb735314ea0743567995d9d68e402 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 21 Sep 2018 23:52:28 -0600 Subject: [PATCH 02/22] fix: fixes for comment counting --- package-lock.json | 5 ++ package.json | 1 + .../server/graph/tenant/schema/schema.graphql | 62 ++++++++++++++----- src/core/server/models/settings.ts | 4 +- src/core/server/models/tenant.ts | 4 +- .../moderation/phases/commentLength.ts | 37 +++++++---- 6 files changed, 82 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index efc1a3a5c..7bdbecbd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23373,6 +23373,11 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, + "striptags": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.1.1.tgz", + "integrity": "sha1-yMPn/db7S7OjKjt1LltePjgJPr0=" + }, "style-loader": { "version": "0.21.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.21.0.tgz", diff --git a/package.json b/package.json index 9a804811f..253e5681a 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "passport-strategy": "^1.0.0", "performance-now": "^2.1.0", "permit": "^0.2.4", + "striptags": "^3.1.1", "subscriptions-transport-ws": "^0.9.12", "tlds": "^1.203.1", "uuid": "^3.3.2" diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 9b808db0d..cbd194806 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -289,6 +289,27 @@ type Karma { thresholds: KarmaThresholds! } +################################################################################ +## CharCount +################################################################################ + +type CharCount { + """ + enabled when true, enables the character count moderation phase. + """ + enabled: Boolean! + + """ + min is the smallest length of a Comment that may be posted. + """ + min: Int + + """ + max is the largest length of a Comment that may be posted. + """ + max: Int +} + ################################################################################ ## Email ################################################################################ @@ -411,14 +432,9 @@ type Settings { editCommentWindowLength: Int! """ - charCountEnable is true when the character count restriction is enabled. + charCount stores the character count moderation settings. """ - charCountEnable: Boolean! - - """ - charCount is the maximum number of characters a comment may be. - """ - charCount: Int + charCount: CharCount! """ organizationName is the name of the organization. @@ -1104,6 +1120,23 @@ input SettingsKarmaInput { thresholds: SettingsKarmaThresholdsInput } +input SettingsCharCountInput { + """ + enabled when true, enables the character count moderation phase. + """ + enabled: Boolean + + """ + min is the smallest length of a Comment that may be posted. + """ + min: Int + + """ + max is the largest length of a Comment that may be posted. + """ + max: Int +} + """ SettingsInput is the partial type of the Settings type for performing mutations. """ @@ -1195,16 +1228,6 @@ input SettingsInput { """ editCommentWindowLength: Int - """ - charCountEnable is true when the character count restriction is enabled. - """ - charCountEnable: Boolean - - """ - charCount is the maximum number of characters a comment may be. - """ - charCount: Int - """ organizationName is the name of the organization. """ @@ -1240,6 +1263,11 @@ input SettingsInput { handled. """ karma: SettingsKarmaInput + + """ + charCount stores the character count moderation settings. + """ + charCount: SettingsCharCountInput } """ diff --git a/src/core/server/models/settings.ts b/src/core/server/models/settings.ts index 451bfdf7a..cf5a3e2c7 100644 --- a/src/core/server/models/settings.ts +++ b/src/core/server/models/settings.ts @@ -1,5 +1,6 @@ import { GQLAuth, + GQLCharCount, GQLEmail, GQLExternalIntegrations, GQLKarma, @@ -62,8 +63,7 @@ export interface ModerationSettings { closedMessage?: string; disableCommenting: boolean; disableCommentingMessage?: string; - charCountEnable: boolean; - charCount?: number; + charCount: GQLCharCount; } export interface Settings extends ModerationSettings { diff --git a/src/core/server/models/tenant.ts b/src/core/server/models/tenant.ts index f67982e6d..7aebe644b 100644 --- a/src/core/server/models/tenant.ts +++ b/src/core/server/models/tenant.ts @@ -71,7 +71,9 @@ export async function createTenant(db: Db, input: CreateTenantInput) { closedTimeout: 60 * 60 * 24 * 7 * 2, disableCommenting: false, editCommentWindowLength: 30 * 1000, - charCountEnable: false, + charCount: { + enabled: false, + }, wordlist: { suspect: [], banned: [], diff --git a/src/core/server/services/comments/moderation/phases/commentLength.ts b/src/core/server/services/comments/moderation/phases/commentLength.ts index cb51f1c56..c48db49fd 100644 --- a/src/core/server/services/comments/moderation/phases/commentLength.ts +++ b/src/core/server/services/comments/moderation/phases/commentLength.ts @@ -1,3 +1,6 @@ +import striptags from "striptags"; + +import { isNil } from "lodash"; import { GQLACTION_GROUP, GQLACTION_TYPE, @@ -9,28 +12,40 @@ import { IntermediatePhaseResult, } from "talk-server/services/comments/moderation"; -const testCharCount = (settings: Partial, length: number) => - settings.charCountEnable && settings.charCount && length > settings.charCount; +const testCharCount = ( + settings: Partial, + length: number +) => { + // settings.charCount.enable && settings.charCount && length > settings.charCount; + + if (settings.charCount && settings.charCount.enabled) { + if (!isNil(settings.charCount.min)) { + if (length < settings.charCount.min) { + return true; + } + } + if (!isNil(settings.charCount.max)) { + if (length > settings.charCount.max) { + return true; + } + } + } + + return false; +}; export const commentLength: IntermediateModerationPhase = ({ asset, tenant, comment, }): IntermediatePhaseResult | void => { - const length = comment.body ? comment.body.length : 0; + const length = comment.body ? striptags(comment.body).length : 0; - // Check to see if the body is too short, if it is, then complain about it! - if (length < 2) { - // TODO: (wyattjoh) return better error. - throw new Error("comment body too short"); - } - - // Reject if the comment is too long + // Reject if the comment is too long or too short. if ( testCharCount(tenant, length) || (asset.settings && testCharCount(asset.settings, length)) ) { - // Add the flag related to Trust to the comment. return { status: GQLCOMMENT_STATUS.REJECTED, actions: [ From 17020405374cc76e59899ed983dccfd5895ee042 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sat, 22 Sep 2018 00:26:09 -0600 Subject: [PATCH 03/22] feat: added parent/parents edges --- package-lock.json | 5 + package.json | 1 + .../server/graph/tenant/loaders/comments.ts | 31 +++- .../server/graph/tenant/resolvers/comment.ts | 30 +++- .../server/graph/tenant/resolvers/util.ts | 12 ++ .../server/graph/tenant/schema/schema.graphql | 16 +++ src/core/server/models/comment.ts | 132 +++++++++++++++++- src/core/server/services/comments/index.ts | 20 ++- src/types/graphql-fields.d.ts | 7 + 9 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 src/core/server/graph/tenant/resolvers/util.ts create mode 100644 src/types/graphql-fields.d.ts diff --git a/package-lock.json b/package-lock.json index 7bdbecbd6..f02f57c0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11379,6 +11379,11 @@ "source-map-support": "^0.5.1" } }, + "graphql-fields": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/graphql-fields/-/graphql-fields-1.1.0.tgz", + "integrity": "sha1-okZtHEFFVNq4DA93d9a6mg4PdbQ=" + }, "graphql-import": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.4.5.tgz", diff --git a/package.json b/package.json index 253e5681a..82ad68c0a 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "fs-extra": "^6.0.1", "graphql": "^0.13.2", "graphql-config": "^2.0.1", + "graphql-fields": "^1.1.0", "graphql-playground-middleware-express": "^1.7.2", "graphql-redis-subscriptions": "^1.5.0", "graphql-tools": "^3.0.5", diff --git a/src/core/server/graph/tenant/loaders/comments.ts b/src/core/server/graph/tenant/loaders/comments.ts index 111a40cd4..94e431acc 100644 --- a/src/core/server/graph/tenant/loaders/comments.ts +++ b/src/core/server/graph/tenant/loaders/comments.ts @@ -3,14 +3,35 @@ import DataLoader from "dataloader"; import Context from "talk-server/graph/tenant/context"; import { AssetToCommentsArgs, + CommentToParentsArgs, CommentToRepliesArgs, GQLCOMMENT_SORT, } from "talk-server/graph/tenant/schema/__generated__/types"; import { + Comment, retrieveCommentAssetConnection, + retrieveCommentParentsConnection, retrieveCommentRepliesConnection, retrieveManyComments, } from "talk-server/models/comment"; +import { Connection } from "talk-server/models/connection"; + +/** + * primeCommentsFromConnection will prime a given context with the comments + * retrieved via a connection. + * + * @param ctx graph context to use to prime the loaders. + */ +const primeCommentsFromConnection = (ctx: Context) => ( + connection: Readonly>> +) => { + // For each of the edges, prime the comment loader. + connection.edges.forEach(({ node }) => { + ctx.loaders.Comments.comment.prime(node.id, node); + }); + + return connection; +}; export default (ctx: Context) => ({ comment: new DataLoader((ids: string[]) => @@ -29,7 +50,7 @@ export default (ctx: Context) => ({ first, orderBy, after, - }), + }).then(primeCommentsFromConnection(ctx)), forParent: ( assetID: string, parentID: string, @@ -50,5 +71,11 @@ export default (ctx: Context) => ({ orderBy, after, } - ), + ).then(primeCommentsFromConnection(ctx)), + parents: (comment: Comment, { last = 1, before }: CommentToParentsArgs) => + retrieveCommentParentsConnection(ctx.mongo, ctx.tenant.id, comment, { + last, + // The cursor passed here is always going to be a number. + before: before as number, + }).then(primeCommentsFromConnection(ctx)), }); diff --git a/src/core/server/graph/tenant/resolvers/comment.ts b/src/core/server/graph/tenant/resolvers/comment.ts index ee775ff0f..8690c5cb4 100644 --- a/src/core/server/graph/tenant/resolvers/comment.ts +++ b/src/core/server/graph/tenant/resolvers/comment.ts @@ -1,4 +1,8 @@ -import { GQLCommentTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; +import { getRequestedFields } from "talk-server/graph/tenant/resolvers/util"; +import { + GQLComment, + GQLCommentTypeResolver, +} from "talk-server/graph/tenant/schema/__generated__/types"; import { Comment } from "talk-server/models/comment"; const Comment: GQLCommentTypeResolver = { @@ -17,6 +21,30 @@ const Comment: GQLCommentTypeResolver = { ctx.loaders.Users.user.load(comment.author_id), replies: (comment, input, ctx) => ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input), + parentCount: comment => + comment.parent_id ? comment.grandparent_ids.length + 1 : 0, + replyCount: comment => comment.child_ids.length, + parent: (comment, input, ctx, info) => { + // If there isn't a parent, then return nothing! + if (!comment.parent_id) { + return null; + } + + // Get the field names of the fields being requested, if it's only the ID, + // we have that, so no need to make a database request. + const fields = getRequestedFields(info); + if (fields.length === 1 && fields[0] === "id") { + return { + id: comment.parent_id, + }; + } + + // We want more than the ID! Get the comment! + // TODO: (wyattjoh) if the parent and the parents (containing the parent) are requested, the parent comment is retrieved from the database twice. Investigate ways of reducing i/o. + return ctx.loaders.Comments.comment.load(comment.parent_id); + }, + parents: (comment, input, ctx) => + ctx.loaders.Comments.parents(comment, input), }; export default Comment; diff --git a/src/core/server/graph/tenant/resolvers/util.ts b/src/core/server/graph/tenant/resolvers/util.ts new file mode 100644 index 000000000..f35b4789d --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/util.ts @@ -0,0 +1,12 @@ +import { GraphQLResolveInfo } from "graphql"; +import graphqlFields from "graphql-fields"; +import { pull } from "lodash"; + +/** + * getRequestedFields returns the fields in an array that are being queried for. + * + * @param info query information + */ +export function getRequestedFields(info: GraphQLResolveInfo) { + return pull(Object.keys(graphqlFields(info)), "__typename"); +} diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index cbd194806..8a3a5e007 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -640,6 +640,11 @@ type Comment { """ status: COMMENT_STATUS! + """ + parentCount is the number of direct parents for this Comment. + """ + parentCount: Int + """ replyCount is the number of replies. Only direct replies to this Comment are counted. Deleted comments are included in this count. @@ -655,6 +660,17 @@ type Comment { after: Cursor ): CommentsConnection + """ + parent is the immediate parent of a given comment. + """ + parent: Comment + + """ + parents returns a CommentsConnection that allows accessing direct parents of + the given Comment. + """ + parents(last: Int = 1, before: Cursor): CommentsConnection + """ editing returns details about the edit status of a Comment. """ diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index bec7cc382..d1dbe3c0e 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -43,7 +43,8 @@ export interface Comment extends TenantResource { status: GQLCOMMENT_STATUS; status_history: StatusHistoryItem[]; action_counts: ActionCounts; - reply_count: number; + grandparent_ids: string[]; + child_ids: string[]; created_at: Date; deleted_at?: Date; metadata?: Record; @@ -54,7 +55,8 @@ export type CreateCommentInput = Omit< | "id" | "tenant_id" | "created_at" - | "reply_count" + // child_ids are always empty for a new comment. + | "child_ids" | "body_history" | "status_history" >; @@ -75,7 +77,7 @@ export async function createComment( id: uuid.v4(), tenant_id: tenantID, created_at: now, - reply_count: 0, + child_ids: [], body_history: [ { body, @@ -102,6 +104,32 @@ export async function createComment( return comment; } +/** + * pushChildCommentIDOntoParent will push the new child comment's ID onto the + * parent comment so it can reference direct children. + */ +export async function pushChildCommentIDOntoParent( + mongo: Db, + tenantID: string, + parentID: string, + childID: string +) { + // This pushes the new child ID onto the parent comment. + const result = await collection(mongo).findOneAndUpdate( + { + tenant_id: tenantID, + id: parentID, + }, + { + $push: { + child_ids: childID, + }, + } + ); + + return result.value; +} + export type EditCommentInput = Pick< Comment, "id" | "author_id" | "body" | "status" | "metadata" @@ -273,6 +301,104 @@ export async function retrieveCommentRepliesConnection( return retrieveConnection(input, query); } +/** + * retrieveCommentParentsConnection will return a comment connection used to + * represent the parents of a given comment. + * + * @param mongo the database connection to use when retrieving comments + * @param tenantID the tenant id for where the comment exists + * @param commentID the id of the comment to retrieve parents of + * @param pagination pagination options to paginate the results + */ +export async function retrieveCommentParentsConnection( + mongo: Db, + tenantID: string, + comment: Comment, + { last: limit, before: skip = -1 }: { last: number; before?: number } +): Promise>>> { + // // Get the base comment, which stores the references to all it's parents. + // const comment = await retrieveComment(mongo, tenantID, commentID); + // if (!comment) { + // throw new Error("comment not found"); + // } + + // Return nothing if this comment does not have any parents. + if (!comment.parent_id) { + return { + edges: [], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + }, + }; + } + + // TODO: (wyattjoh) maybe throw an error when the limit is zero? + + if (limit <= 0) { + return { + edges: [], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + }, + }; + } + + // If the last paramter is 1, and the after paramter is either unset or equal + // to zero, then all we have to return is the direct parent. + if (limit === 1 && skip < 0) { + const parent = await retrieveComment(mongo, tenantID, comment.parent_id); + if (!parent) { + throw new Error("parent comment not found"); + } + + return { + edges: nodesToEdges([parent], () => -1), + pageInfo: { + hasNextPage: false, + hasPreviousPage: comment.grandparent_ids.length > 0, + endCursor: 0, + startCursor: 0, + }, + }; + } + + // Create a list of all the comment parent ids, in reverse order. + const parentIDs = [comment.parent_id, ...comment.grandparent_ids.reverse()]; + + // Fetch the subset of the comment id's that we are going to query for. + const parentIDSubset = parentIDs.slice(skip + 1, skip + 1 + limit); + + // Retrieve the parents via the subset list. + const parents = await retrieveManyComments(mongo, tenantID, parentIDSubset); + + // Loop over the list to ensure that none of the entries is null (indicating + // that there was a misplaced parent). We can assert the type here because we + // will throw an error and abort if one of the comments are null. + const nodes = parents.filter(parentComment => { + if (!parentComment) { + // TODO: (wyattjoh) replace with a better error. + throw new Error("parent id specified does not exist"); + } + + return true; + }) as Array>; + + const edges = nodesToEdges(nodes, (_, index) => index + skip + 1).reverse(); + + // Return the resolved connection. + return { + edges, + pageInfo: { + hasNextPage: false, + hasPreviousPage: parentIDs.length > limit + skip, + startCursor: edges.length > 0 ? edges[0].cursor : null, + endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null, + }, + }; +} + /** * retrieveAssetConnection returns a Connection for a given Asset's * comments. diff --git a/src/core/server/services/comments/index.ts b/src/core/server/services/comments/index.ts index 25a264f5a..4d9151818 100644 --- a/src/core/server/services/comments/index.ts +++ b/src/core/server/services/comments/index.ts @@ -7,6 +7,7 @@ import { CreateCommentInput, editComment, EditCommentInput, + pushChildCommentIDOntoParent, retrieveComment, } from "talk-server/models/comment"; import { Tenant } from "talk-server/models/tenant"; @@ -16,7 +17,7 @@ import { Request } from "talk-server/types/express"; export type CreateComment = Omit< CreateCommentInput, - "status" | "action_counts" | "metadata" + "status" | "action_counts" | "metadata" | "grandparent_ids" >; export async function create( @@ -35,6 +36,7 @@ export async function create( // TODO: (wyattjoh) Check that the asset was visible. + const grandparentIDs: string[] = []; if (input.parent_id) { // Check to see that the reference parent ID exists. const parent = await retrieveComment(mongo, tenant.id, input.parent_id); @@ -44,6 +46,13 @@ export async function create( } // TODO: (wyattjoh) Check that the parent comment was visible. + + // Push the parent's parent id's into the comment's grandparent id's. + grandparentIDs.push(...parent.grandparent_ids); + if (parent.parent_id) { + // If this parent has a parent, push it down as well. + grandparentIDs.push(parent.parent_id); + } } // Run the comment through the moderation phases. @@ -61,11 +70,18 @@ export async function create( ...input, status, action_counts: {}, + grandparent_ids: grandparentIDs, metadata, }); if (input.parent_id) { - // TODO: update reply count of parent. + // Push the child's ID onto the parent. + await pushChildCommentIDOntoParent( + mongo, + tenant.id, + input.parent_id, + comment.id + ); } return comment; diff --git a/src/types/graphql-fields.d.ts b/src/types/graphql-fields.d.ts new file mode 100644 index 000000000..c5a9ee9f0 --- /dev/null +++ b/src/types/graphql-fields.d.ts @@ -0,0 +1,7 @@ +declare module "graphql-fields" { + import { GraphQLResolveInfo } from "graphql"; + + export default function graphqlFields( + info: GraphQLResolveInfo + ): { [P in keyof T]: any }; +} From ae60311e856700b9cce538f4324c298ac3f3e7e1 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sat, 22 Sep 2018 00:45:48 -0600 Subject: [PATCH 04/22] feat: added root parent edge --- .../server/graph/tenant/resolvers/comment.ts | 25 +++++++++++++++++++ .../server/graph/tenant/schema/schema.graphql | 5 ++++ 2 files changed, 30 insertions(+) diff --git a/src/core/server/graph/tenant/resolvers/comment.ts b/src/core/server/graph/tenant/resolvers/comment.ts index 8690c5cb4..9272a294c 100644 --- a/src/core/server/graph/tenant/resolvers/comment.ts +++ b/src/core/server/graph/tenant/resolvers/comment.ts @@ -24,6 +24,31 @@ const Comment: GQLCommentTypeResolver = { parentCount: comment => comment.parent_id ? comment.grandparent_ids.length + 1 : 0, replyCount: comment => comment.child_ids.length, + rootParent: (comment, input, ctx, info) => { + // If there isn't a parent, then return nothing! + if (!comment.parent_id) { + return null; + } + + // rootParentID is the root parent id for a given comment. + const rootParentID = + comment.grandparent_ids.length > 0 + ? comment.grandparent_ids[0] + : comment.parent_id; + + // Get the field names of the fields being requested, if it's only the ID, + // we have that, so no need to make a database request. + const fields = getRequestedFields(info); + if (fields.length === 1 && fields[0] === "id") { + return { + id: rootParentID, + }; + } + + // We want more than the ID! Get the comment! + // TODO: (wyattjoh) if the parent and the parents (containing the parent) are requested, the parent comment is retrieved from the database twice. Investigate ways of reducing i/o. + return ctx.loaders.Comments.comment.load(rootParentID); + }, parent: (comment, input, ctx, info) => { // If there isn't a parent, then return nothing! if (!comment.parent_id) { diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 8a3a5e007..4b2d9ca23 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -665,6 +665,11 @@ type Comment { """ parent: Comment + """ + rootParent is the comment that is not a reply to another Comment. + """ + rootParent: Comment + """ parents returns a CommentsConnection that allows accessing direct parents of the given Comment. From 2edd923b9e1fac3de6d7821aeb7baf07df92a0f0 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sat, 22 Sep 2018 00:45:58 -0600 Subject: [PATCH 05/22] feat: prime current user in user cache --- src/core/server/graph/tenant/loaders/users.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/core/server/graph/tenant/loaders/users.ts b/src/core/server/graph/tenant/loaders/users.ts index c684a9d23..2b070c6a0 100644 --- a/src/core/server/graph/tenant/loaders/users.ts +++ b/src/core/server/graph/tenant/loaders/users.ts @@ -2,8 +2,17 @@ import DataLoader from "dataloader"; import Context from "talk-server/graph/tenant/context"; import { retrieveManyUsers, User } from "talk-server/models/user"; -export default (ctx: Context) => ({ - user: new DataLoader(ids => +export default (ctx: Context) => { + const user = new DataLoader(ids => retrieveManyUsers(ctx.mongo, ctx.tenant.id, ids) - ), -}); + ); + + if (ctx.user) { + // Prime the current logged in user in the dataloader cache. + user.prime(ctx.user.id, ctx.user); + } + + return { + user, + }; +}; From 32fdc515064d87301a6ac7d1efb8a688cea72d40 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sat, 22 Sep 2018 00:49:48 -0600 Subject: [PATCH 06/22] fix: updated comment --- src/core/server/graph/tenant/schema/schema.graphql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 4b2d9ca23..142d59973 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -666,7 +666,8 @@ type Comment { parent: Comment """ - rootParent is the comment that is not a reply to another Comment. + rootParent is the highest level parent Comment. This Comment would have been + left on the Asset itself. """ rootParent: Comment From 50a691c1e525454a996dde2f6c99f20b0417e85d Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sat, 22 Sep 2018 00:50:55 -0600 Subject: [PATCH 07/22] fix: removed dead code comment --- src/core/server/models/comment.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index d1dbe3c0e..c7acd2b61 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -316,12 +316,6 @@ export async function retrieveCommentParentsConnection( comment: Comment, { last: limit, before: skip = -1 }: { last: number; before?: number } ): Promise>>> { - // // Get the base comment, which stores the references to all it's parents. - // const comment = await retrieveComment(mongo, tenantID, commentID); - // if (!comment) { - // throw new Error("comment not found"); - // } - // Return nothing if this comment does not have any parents. if (!comment.parent_id) { return { From 8589993401851e998554359db5b6adac6f523fb6 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sat, 22 Sep 2018 00:57:40 -0600 Subject: [PATCH 08/22] fix: addressed cursor issue --- 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 c7acd2b61..3e2b06cb4 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -348,7 +348,7 @@ export async function retrieveCommentParentsConnection( } return { - edges: nodesToEdges([parent], () => -1), + edges: [{ node: parent, cursor: 0 }], pageInfo: { hasNextPage: false, hasPreviousPage: comment.grandparent_ids.length > 0, From a8ccf29bea9d0996c006f4fe362ef7d115147588 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sun, 23 Sep 2018 17:28:15 -0600 Subject: [PATCH 09/22] fix: fixed issues with sorting --- .../server/graph/tenant/resolvers/comment.ts | 14 ++++++++-- .../server/graph/tenant/schema/schema.graphql | 21 +++++++------- src/core/server/models/comment.ts | 28 +++++++++---------- src/core/server/models/connection.ts | 21 ++++++++++++++ 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/src/core/server/graph/tenant/resolvers/comment.ts b/src/core/server/graph/tenant/resolvers/comment.ts index 9272a294c..d93612354 100644 --- a/src/core/server/graph/tenant/resolvers/comment.ts +++ b/src/core/server/graph/tenant/resolvers/comment.ts @@ -4,6 +4,7 @@ import { GQLCommentTypeResolver, } from "talk-server/graph/tenant/schema/__generated__/types"; import { Comment } from "talk-server/models/comment"; +import { createConnection } from "talk-server/models/connection"; const Comment: GQLCommentTypeResolver = { editing: (comment, input, ctx) => ({ @@ -20,10 +21,14 @@ const Comment: GQLCommentTypeResolver = { author: (comment, input, ctx) => ctx.loaders.Users.user.load(comment.author_id), replies: (comment, input, ctx) => - ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input), + comment.reply_count > 0 + ? ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input) + : createConnection(), parentCount: comment => comment.parent_id ? comment.grandparent_ids.length + 1 : 0, - replyCount: comment => comment.child_ids.length, + depth: comment => + comment.parent_id ? comment.grandparent_ids.length + 1 : 0, + replyCount: comment => comment.reply_count, rootParent: (comment, input, ctx, info) => { // If there isn't a parent, then return nothing! if (!comment.parent_id) { @@ -69,7 +74,10 @@ const Comment: GQLCommentTypeResolver = { return ctx.loaders.Comments.comment.load(comment.parent_id); }, parents: (comment, input, ctx) => - ctx.loaders.Comments.parents(comment, input), + // Some resolver optimization. + comment.parent_id + ? ctx.loaders.Comments.parents(comment, input) + : createConnection(), }; export default Comment; diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 142d59973..fdc17718c 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -641,15 +641,21 @@ type Comment { status: COMMENT_STATUS! """ - parentCount is the number of direct parents for this Comment. + parentCount is the number of direct parents for this Comment. Currently this + value is the same as depth. """ - parentCount: Int + parentCount: Int! + + """ + depth is the number of levels that a given comment is deep. + """ + depth: Int! """ replyCount is the number of replies. Only direct replies to this Comment are counted. Deleted comments are included in this count. """ - replyCount: Int + replyCount: Int! """ replies will return the replies to this Comment. @@ -658,7 +664,7 @@ type Comment { first: Int = 10 orderBy: COMMENT_SORT = CREATED_AT_DESC after: Cursor - ): CommentsConnection + ): CommentsConnection! """ parent is the immediate parent of a given comment. @@ -675,7 +681,7 @@ type Comment { parents returns a CommentsConnection that allows accessing direct parents of the given Comment. """ - parents(last: Int = 1, before: Cursor): CommentsConnection + parents(last: Int = 1, before: Cursor): CommentsConnection! """ editing returns details about the edit status of a Comment. @@ -835,11 +841,6 @@ type Query { """ comment(id: ID!): Comment - """ - assets returns a AssetsConnection. - """ - assets(cursor: Cursor, limit: Int = 10): AssetsConnection - """ asset is the Asset specified by its ID/URL. """ diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index 3e2b06cb4..a780b24ad 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -10,6 +10,7 @@ import { import { ActionCounts } from "talk-server/models/actions"; import { Connection, + createConnection, Cursor, getPageInfo, nodesToEdges, @@ -28,7 +29,7 @@ export interface BodyHistoryItem { } export interface StatusHistoryItem { - status: GQLCOMMENT_STATUS; // TODO: migrate field + status: GQLCOMMENT_STATUS; assigned_by?: string; created_at: Date; } @@ -44,7 +45,8 @@ export interface Comment extends TenantResource { status_history: StatusHistoryItem[]; action_counts: ActionCounts; grandparent_ids: string[]; - child_ids: string[]; + reply_ids: string[]; + reply_count: number; created_at: Date; deleted_at?: Date; metadata?: Record; @@ -55,8 +57,8 @@ export type CreateCommentInput = Omit< | "id" | "tenant_id" | "created_at" - // child_ids are always empty for a new comment. - | "child_ids" + | "reply_ids" + | "reply_count" | "body_history" | "status_history" >; @@ -77,7 +79,8 @@ export async function createComment( id: uuid.v4(), tenant_id: tenantID, created_at: now, - child_ids: [], + reply_ids: [], + reply_count: 0, body_history: [ { body, @@ -121,9 +124,8 @@ export async function pushChildCommentIDOntoParent( id: parentID, }, { - $push: { - child_ids: childID, - }, + $push: { reply_ids: childID }, + $inc: { reply_count: 1 }, } ); @@ -318,25 +320,23 @@ export async function retrieveCommentParentsConnection( ): Promise>>> { // Return nothing if this comment does not have any parents. if (!comment.parent_id) { - return { - edges: [], + return createConnection({ pageInfo: { hasNextPage: false, hasPreviousPage: false, }, - }; + }); } // TODO: (wyattjoh) maybe throw an error when the limit is zero? if (limit <= 0) { - return { - edges: [], + return createConnection({ pageInfo: { hasNextPage: false, hasPreviousPage: false, }, - }; + }); } // If the last paramter is 1, and the after paramter is either unset or equal diff --git a/src/core/server/models/connection.ts b/src/core/server/models/connection.ts index e45e9657a..3eeacd937 100644 --- a/src/core/server/models/connection.ts +++ b/src/core/server/models/connection.ts @@ -1,3 +1,5 @@ +import { merge } from "lodash"; + export type Cursor = Date | number | string | null; export interface Edge { @@ -17,6 +19,25 @@ export interface Connection { pageInfo: PageInfo; } +/** + * createConnection will create a base Connection that can be used to satisfy + * the Connection interface. + * + * @param connection the base connection to optionally merge with the default base + * connection details. + */ +export function createConnection( + connection: Partial> = {} +): Connection { + return merge( + { + edges: [], + pageInfo: {}, + }, + connection + ); +} + export interface PaginationArgs { first: number; } From a8f315db4b7face275748b97612d46d23aa5e7e2 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sun, 23 Sep 2018 18:08:24 -0600 Subject: [PATCH 10/22] fix: test patches --- src/core/client/stream/test/comments/postComment.spec.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/client/stream/test/comments/postComment.spec.tsx b/src/core/client/stream/test/comments/postComment.spec.tsx index 01f296ffc..133f0976e 100644 --- a/src/core/client/stream/test/comments/postComment.spec.tsx +++ b/src/core/client/stream/test/comments/postComment.spec.tsx @@ -39,6 +39,7 @@ beforeEach(() => { edited: false, editableUntil: "2018-07-06T18:24:30.000Z", }, + replies: { edges: [], pageInfo: {} }, }, }, clientMutationId: "0", From d072df80436a129a362caf6b139135861c59170f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sun, 23 Sep 2018 18:16:47 -0600 Subject: [PATCH 11/22] fix: test fixes --- .../tabs/comments/containers/ReplyListContainer.spec.tsx | 4 ++-- .../containers/__snapshots__/ReplyListContainer.spec.tsx.snap | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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..6d3a4d44c 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.spec.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.spec.tsx @@ -34,14 +34,14 @@ it("renders correctly", () => { expect(wrapper).toMatchSnapshot(); }); -it("renders correctly when replies are null", () => { +it("renders correctly when replies are empty", () => { const props: PropTypesOf = { asset: { id: "asset-id", }, comment: { id: "comment-id", - replies: null, + replies: { edges: [] }, }, relay: { hasMore: noop, 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..94b68d625 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 @@ -44,7 +44,7 @@ exports[`renders correctly 1`] = ` /> `; -exports[`renders correctly when replies are null 1`] = `""`; +exports[`renders correctly when replies are empty 1`] = `""`; exports[`when has more replies renders hasMore 1`] = ` Date: Mon, 24 Sep 2018 20:08:11 +0200 Subject: [PATCH 12/22] 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 13/22] 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 14/22] 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 15/22] 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 7139563ceb42adefd96a2f266b150c1892e9cc54 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 24 Sep 2018 20:36:39 +0200 Subject: [PATCH 16/22] 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 17/22] 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 8c2d811b55f6229be0659bbc118923616713212a Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 24 Sep 2018 22:04:03 +0200 Subject: [PATCH 18/22] 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 460429d99143f1034f19187166751ee44107c743 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 24 Sep 2018 22:11:03 +0200 Subject: [PATCH 19/22] 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 20/22] 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 bd56a39d73af12aeeb0ace2dbb77df40d5e12ef1 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 25 Sep 2018 10:06:05 -0600 Subject: [PATCH 21/22] review: fixes --- src/core/server/models/comment.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index a780b24ad..18c7b8cf7 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -370,16 +370,20 @@ export async function retrieveCommentParentsConnection( // Loop over the list to ensure that none of the entries is null (indicating // that there was a misplaced parent). We can assert the type here because we // will throw an error and abort if one of the comments are null. - const nodes = parents.filter(parentComment => { + parents.forEach(parentComment => { if (!parentComment) { // TODO: (wyattjoh) replace with a better error. throw new Error("parent id specified does not exist"); } return true; - }) as Array>; + }); - const edges = nodesToEdges(nodes, (_, index) => index + skip + 1).reverse(); + const edges = nodesToEdges( + // We can't have a null parent after the forEach filter above. + parents as Array>, + (_, index) => index + skip + 1 + ).reverse(); // Return the resolved connection. return { From 04b4b7e715ada0360204e377b0f0fc72cccf322f Mon Sep 17 00:00:00 2001 From: Kiwi Date: Tue, 25 Sep 2018 18:25:42 +0200 Subject: [PATCH 22/22] 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",