diff --git a/src/core/client/stream/helpers/incrementStoryCommentCounts.ts b/src/core/client/stream/helpers/incrementStoryCommentCounts.ts new file mode 100644 index 000000000..d25c2c603 --- /dev/null +++ b/src/core/client/stream/helpers/incrementStoryCommentCounts.ts @@ -0,0 +1,17 @@ +import { RecordSourceSelectorProxy } from "relay-runtime"; + +export default function incrementStoryCommentCounts( + store: RecordSourceSelectorProxy, + storyID: string +) { + // Updating Comment Count + const story = store.get(storyID); + if (story) { + const record = story.getLinkedRecord("commentCounts"); + if (record) { + // TODO: when we have moderation, we'll need to be careful here. + const currentCount = record.getValue("totalVisible"); + record.setValue(currentCount + 1, "totalVisible"); + } + } +} diff --git a/src/core/client/stream/helpers/index.ts b/src/core/client/stream/helpers/index.ts new file mode 100644 index 000000000..32b82ca36 --- /dev/null +++ b/src/core/client/stream/helpers/index.ts @@ -0,0 +1,6 @@ +export { + default as incrementStoryCommentCounts, +} from "./incrementStoryCommentCounts"; +export { + default as prependCommentEdgeToProfile, +} from "./prependCommentEdgeToProfile"; diff --git a/src/core/client/stream/helpers/prependCommentEdgeToProfile.ts b/src/core/client/stream/helpers/prependCommentEdgeToProfile.ts new file mode 100644 index 000000000..edae8cacf --- /dev/null +++ b/src/core/client/stream/helpers/prependCommentEdgeToProfile.ts @@ -0,0 +1,25 @@ +import { + ConnectionHandler, + Environment, + RecordProxy, + RecordSourceSelectorProxy, +} from "relay-runtime"; + +import { getMeSourceID } from "talk-framework/helpers"; + +export default function prependCommentEdgeToProfile( + environment: Environment, + store: RecordSourceSelectorProxy, + commentEdge: RecordProxy +) { + const meProxy = store.get(getMeSourceID(environment)!); + const con = ConnectionHandler.getConnection( + meProxy, + "CommentHistory_comments" + ); + // Note: Currently this is always null, until Relay comes + // with better data retaintion and data from store support. + if (con) { + ConnectionHandler.insertEdgeBefore(con, commentEdge); + } +} diff --git a/src/core/client/stream/mutations/CreateCommentMutation.ts b/src/core/client/stream/mutations/CreateCommentMutation.ts index f16fa0880..c1e71c936 100644 --- a/src/core/client/stream/mutations/CreateCommentMutation.ts +++ b/src/core/client/stream/mutations/CreateCommentMutation.ts @@ -5,144 +5,69 @@ import { RecordSourceSelectorProxy, } from "relay-runtime"; -import { getMe, getMeSourceID } from "talk-framework/helpers"; +import { getMe } from "talk-framework/helpers"; import { TalkContext } from "talk-framework/lib/bootstrap"; import { commitMutationPromiseNormalized, createMutationContainer, } from "talk-framework/lib/relay"; import { Omit } from "talk-framework/types"; - import { CreateCommentMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentMutation.graphql"; +import { + incrementStoryCommentCounts, + prependCommentEdgeToProfile, +} from "../helpers"; + export type CreateCommentInput = Omit< MutationTypes["variables"]["input"], "clientMutationId" -> & { local?: boolean }; +>; function sharedUpdater( environment: Environment, store: RecordSourceSelectorProxy, input: CreateCommentInput ) { - updateStory(store, input); - if (input.local) { - localUpdate(store, input); - } else { - update(store, input); - } - updateProfile(environment, store, input); -} - -function updateStory( - store: RecordSourceSelectorProxy, - input: CreateCommentInput -) { - // Updating Comment Count - const story = store.get(input.storyID); - if (story) { - const record = story.getLinkedRecord("commentCounts"); - if (record) { - // TODO: when we have moderation, we'll need to be careful here. - const currentCount = record.getValue("totalVisible"); - record.setValue(currentCount + 1, "totalVisible"); - } - } + incrementStoryCommentCounts(store, input.storyID); + prependCommentEdgeToProfile( + environment, + store, + store.getRootField("createComment")!.getLinkedRecord("edge")! + ); + addCommentToStory(store, input); } /** * update integrates new comment into the CommentConnection. */ -function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) { +function addCommentToStory( + 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.storyID); + // Get stream proxy. + const streamProxy = store.get(input.storyID); + const connectionKey = "Stream_comments"; + const filters = { orderBy: "CREATED_AT_DESC" }; - 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) { + if (streamProxy) { const con = ConnectionHandler.getConnection( - parentProxy, + streamProxy, connectionKey, filters ); if (con) { - if (where === "prepend") { - ConnectionHandler.insertEdgeBefore(con, newEdge); - } else { - ConnectionHandler.insertEdgeAfter(con, newEdge); - } + ConnectionHandler.insertEdgeBefore(con, newEdge); } } } -/** - * localUpdate is like update but updates the `localReplies` endpoint. - */ -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"); - } -} - -/** - * updateProfile integrates new comment into the profile. - */ -function updateProfile( - environment: Environment, - 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 meProxy = store.get(getMeSourceID(environment)!); - const con = ConnectionHandler.getConnection( - meProxy, - "CommentHistory_comments" - ); - // Note: Currently this is always null, until Relay comes - // with better data retaintion and data from store support. - if (con) { - ConnectionHandler.insertEdgeBefore(con, newEdge); - } -} - const mutation = graphql` mutation CreateCommentMutation($input: CreateCommentInput!) { createComment(input: $input) { @@ -172,7 +97,6 @@ function commit( variables: { input: { storyID: input.storyID, - parentID: input.parentID, body: input.body, clientMutationId: clientMutationId.toString(), }, diff --git a/src/core/client/stream/mutations/CreateCommentReplyMutation.ts b/src/core/client/stream/mutations/CreateCommentReplyMutation.ts new file mode 100644 index 000000000..8d2fecaa4 --- /dev/null +++ b/src/core/client/stream/mutations/CreateCommentReplyMutation.ts @@ -0,0 +1,178 @@ +import { graphql } from "react-relay"; +import { + ConnectionHandler, + Environment, + RecordSourceSelectorProxy, +} from "relay-runtime"; + +import { getMe } from "talk-framework/helpers"; +import { TalkContext } from "talk-framework/lib/bootstrap"; +import { + commitMutationPromiseNormalized, + createMutationContainer, +} from "talk-framework/lib/relay"; +import { Omit } from "talk-framework/types"; +import { CreateCommentReplyMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentReplyMutation.graphql"; + +import { + incrementStoryCommentCounts, + prependCommentEdgeToProfile, +} from "../helpers"; + +export type CreateCommentReplyInput = Omit< + MutationTypes["variables"]["input"], + "clientMutationId" +> & { local?: boolean }; + +function sharedUpdater( + environment: Environment, + store: RecordSourceSelectorProxy, + input: CreateCommentReplyInput +) { + incrementStoryCommentCounts(store, input.storyID); + prependCommentEdgeToProfile( + environment, + store, + store.getRootField("createCommentReply")!.getLinkedRecord("edge")! + ); + if (input.local) { + addLocalCommentReplyToStory(store, input); + } else { + addCommentReplyToStory(store, input); + } +} + +/** + * update integrates new comment into the CommentConnection. + */ +function addCommentReplyToStory( + store: RecordSourceSelectorProxy, + input: CreateCommentReplyInput +) { + // Get the payload returned from the server. + const payload = store.getRootField("createCommentReply")!; + + // Get the edge of the newly created comment. + const newEdge = payload.getLinkedRecord("edge")!; + + // Get parent proxy. + const parentProxy = store.get(input.parentID); + const connectionKey = "ReplyList_replies"; + const filters = { orderBy: "CREATED_AT_ASC" }; + + if (parentProxy) { + const con = ConnectionHandler.getConnection( + parentProxy, + connectionKey, + filters + ); + if (con) { + ConnectionHandler.insertEdgeAfter(con, newEdge); + } + } +} + +/** + * localUpdate is like update but updates the `localReplies` endpoint. + */ +function addLocalCommentReplyToStory( + store: RecordSourceSelectorProxy, + input: CreateCommentReplyInput +) { + // Get the payload returned from the server. + const payload = store.getRootField("createCommentReply")!; + + // 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 CreateCommentReplyMutation($input: CreateCommentReplyInput!) { + createCommentReply(input: $input) { + edge { + cursor + node { + ...StreamContainer_comment @relay(mask: false) + } + } + clientMutationId + } + } +`; + +let clientMutationId = 0; + +function commit( + environment: Environment, + input: CreateCommentReplyInput, + { uuidGenerator }: TalkContext +) { + const me = getMe(environment)!; + const currentDate = new Date().toISOString(); + const id = uuidGenerator(); + return commitMutationPromiseNormalized(environment, { + mutation, + variables: { + input: { + storyID: input.storyID, + parentID: input.parentID, + parentRevisionID: input.parentRevisionID, + body: input.body, + clientMutationId: clientMutationId.toString(), + }, + }, + optimisticResponse: { + createCommentReply: { + edge: { + cursor: currentDate, + node: { + id, + createdAt: currentDate, + author: { + id: me.id, + username: me.username, + }, + body: input.body, + editing: { + editableUntil: new Date(Date.now() + 10000), + }, + actionCounts: { + reaction: { + total: 0, + }, + }, + }, + }, + clientMutationId: (clientMutationId++).toString(), + }, + } as any, // TODO: (cvle) generated types should contain one for the optimistic response. + optimisticUpdater: store => { + sharedUpdater(environment, store, input); + store.get(id)!.setValue(true, "pending"); + }, + updater: store => { + sharedUpdater(environment, store, input); + }, + }); +} + +export const withCreateCommentReplyMutation = createMutationContainer( + "createCommentReply", + commit +); + +export type CreateCommentReplyMutation = ( + input: CreateCommentReplyInput +) => Promise; diff --git a/src/core/client/stream/mutations/index.ts b/src/core/client/stream/mutations/index.ts index 5fa3e2e98..0669b0a83 100644 --- a/src/core/client/stream/mutations/index.ts +++ b/src/core/client/stream/mutations/index.ts @@ -3,6 +3,11 @@ export { CreateCommentMutation, CreateCommentInput, } from "./CreateCommentMutation"; +export { + withCreateCommentReplyMutation, + CreateCommentReplyMutation, + CreateCommentReplyInput, +} from "./CreateCommentReplyMutation"; export { withEditCommentMutation, EditCommentMutation, 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 902393d6c..9042a3866 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.spec.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.spec.tsx @@ -19,7 +19,7 @@ function getContextKey(commentID: string) { it("renders correctly", async () => { const props: PropTypesOf = { - createComment: noop as any, + createCommentReply: noop as any, story: { id: "story-id", }, @@ -28,6 +28,9 @@ it("renders correctly", async () => { author: { username: "Joe", }, + revision: { + id: "revision-id", + }, }, sessionStorage: createPromisifiedStorage(), autofocus: false, @@ -41,7 +44,7 @@ it("renders correctly", async () => { it("renders with initialValues", async () => { const props: PropTypesOf = { - createComment: noop as any, + createCommentReply: noop as any, story: { id: "story-id", }, @@ -50,6 +53,9 @@ it("renders with initialValues", async () => { author: { username: "Joe", }, + revision: { + id: "revision-id", + }, }, sessionStorage: createPromisifiedStorage(), autofocus: false, @@ -68,7 +74,7 @@ it("renders with initialValues", async () => { it("save values", async () => { const props: PropTypesOf = { - createComment: noop as any, + createCommentReply: noop as any, story: { id: "story-id", }, @@ -77,6 +83,9 @@ it("save values", async () => { author: { username: "Joe", }, + revision: { + id: "revision-id", + }, }, sessionStorage: createPromisifiedStorage(), autofocus: false, @@ -107,7 +116,7 @@ it("creates a comment", async () => { const onCloseStub = sinon.stub(); const props: PropTypesOf = { - createComment: createCommentStub, + createCommentReply: createCommentStub, story: { id: "story-id", }, @@ -116,6 +125,9 @@ it("creates a comment", async () => { author: { username: "Joe", }, + revision: { + id: "revision-id", + }, }, sessionStorage: createPromisifiedStorage(), onClose: onCloseStub, @@ -138,6 +150,7 @@ it("creates a comment", async () => { createCommentStub.calledWith({ storyID, parentID: props.comment.id, + parentRevisionID: "revision-id", ...input, }) ).toBeTruthy(); @@ -148,7 +161,7 @@ it("creates a comment", async () => { it("closes on cancel", async () => { const onCloseStub = sinon.stub(); const props: PropTypesOf = { - createComment: noop as any, + createCommentReply: noop as any, story: { id: "story-id", }, @@ -157,6 +170,9 @@ it("closes on cancel", async () => { author: { username: "Joe", }, + revision: { + id: "revision-id", + }, }, sessionStorage: createPromisifiedStorage(), onClose: onCloseStub, @@ -186,7 +202,7 @@ it("autofocuses", async () => { const focusStub = sinon.stub(); const rte = { focus: focusStub }; const props: PropTypesOf = { - createComment: noop as any, + createCommentReply: noop as any, story: { id: "story-id", }, @@ -195,6 +211,9 @@ it("autofocuses", async () => { author: { username: "Joe", }, + revision: { + id: "revision-id", + }, }, sessionStorage: createPromisifiedStorage(), autofocus: true, diff --git a/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.tsx b/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.tsx index f309b6d29..5abaf5d62 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.tsx @@ -10,8 +10,8 @@ import { PropTypesOf } from "talk-framework/types"; import { ReplyCommentFormContainer_comment as CommentData } from "talk-stream/__generated__/ReplyCommentFormContainer_comment.graphql"; import { ReplyCommentFormContainer_story as StoryData } from "talk-stream/__generated__/ReplyCommentFormContainer_story.graphql"; import { - CreateCommentMutation, - withCreateCommentMutation, + CreateCommentReplyMutation, + withCreateCommentReplyMutation, } from "talk-stream/mutations"; import ReplyCommentForm, { @@ -19,7 +19,7 @@ import ReplyCommentForm, { } from "../components/ReplyCommentForm"; interface InnerProps { - createComment: CreateCommentMutation; + createCommentReply: CreateCommentReplyMutation; sessionStorage: PromisifiedStorage; comment: CommentData; story: StoryData; @@ -74,9 +74,10 @@ export class ReplyCommentFormContainer extends Component { form ) => { try { - await this.props.createComment({ + await this.props.createCommentReply({ storyID: this.props.story.id, parentID: this.props.comment.id, + parentRevisionID: this.props.comment.revision.id, local: this.props.localReply, ...input, }); @@ -127,7 +128,7 @@ const enhanced = withContext(({ sessionStorage, browserInfo }) => ({ // Disable autofocus on ios and enable for the rest. autofocus: !browserInfo.ios, }))( - withCreateCommentMutation( + withCreateCommentReplyMutation( withFragmentContainer({ story: graphql` fragment ReplyCommentFormContainer_story on Story { @@ -140,6 +141,9 @@ const enhanced = withContext(({ sessionStorage, browserInfo }) => ({ author { username } + revision { + id + } } `, })(ReplyCommentFormContainer) diff --git a/src/core/client/stream/test/comments/postLocalReply.spec.tsx b/src/core/client/stream/test/comments/postLocalReply.spec.tsx index e56f23d16..326add8c4 100644 --- a/src/core/client/stream/test/comments/postLocalReply.spec.tsx +++ b/src/core/client/stream/test/comments/postLocalReply.spec.tsx @@ -28,7 +28,7 @@ beforeEach(() => { ), }, Mutation: { - createComment: createSinonStub( + createCommentReply: createSinonStub( s => s.throws(), s => s @@ -36,6 +36,7 @@ beforeEach(() => { input: { storyID: storyWithDeepestReplies.id, parentID: "comment-with-deepest-replies-5", + parentRevisionID: "revision-0", body: "Hello world!", clientMutationId: "0", }, diff --git a/src/core/client/stream/test/comments/postReply.spec.tsx b/src/core/client/stream/test/comments/postReply.spec.tsx index 17baf2a1c..7aab966cf 100644 --- a/src/core/client/stream/test/comments/postReply.spec.tsx +++ b/src/core/client/stream/test/comments/postReply.spec.tsx @@ -23,7 +23,7 @@ beforeEach(() => { ), }, Mutation: { - createComment: createSinonStub( + createCommentReply: createSinonStub( s => s.throws(), s => s @@ -31,6 +31,7 @@ beforeEach(() => { input: { storyID: stories[0].id, parentID: stories[0].comments.edges[0].node.id, + parentRevisionID: stories[0].comments.edges[0].node.revision.id, body: "Hello world!", clientMutationId: "0", }, diff --git a/src/core/server/graph/tenant/mutators/Comment.ts b/src/core/server/graph/tenant/mutators/Comment.ts index 9c744babd..1b984df89 100644 --- a/src/core/server/graph/tenant/mutators/Comment.ts +++ b/src/core/server/graph/tenant/mutators/Comment.ts @@ -4,6 +4,7 @@ import { GQLCreateCommentFlagInput, GQLCreateCommentInput, GQLCreateCommentReactionInput, + GQLCreateCommentReplyInput, GQLEditCommentInput, GQLRemoveCommentDontAgreeInput, GQLRemoveCommentFlagInput, @@ -20,12 +21,15 @@ import { } from "talk-server/services/comments/actions"; export const Comment = (ctx: TenantContext) => ({ - create: ({ storyID, body, parentID }: GQLCreateCommentInput) => + create: ({ + clientMutationId, + ...comment + }: GQLCreateCommentInput | GQLCreateCommentReplyInput) => create( ctx.mongo, ctx.tenant, ctx.user!, - { authorID: ctx.user!.id, storyID, body, parentID }, + { authorID: ctx.user!.id, ...comment }, ctx.req ), edit: ({ commentID, body }: GQLEditCommentInput) => diff --git a/src/core/server/graph/tenant/resolvers/Mutation.ts b/src/core/server/graph/tenant/resolvers/Mutation.ts index ec0303145..ccd54174c 100644 --- a/src/core/server/graph/tenant/resolvers/Mutation.ts +++ b/src/core/server/graph/tenant/resolvers/Mutation.ts @@ -1,24 +1,30 @@ import { GQLMutationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; -export const Mutation: GQLMutationTypeResolver = { +export const Mutation: Required> = { editComment: async (source, { input }, ctx) => ({ comment: await ctx.mutators.Comment.edit(input), clientMutationId: input.clientMutationId, }), - createComment: async (source, { input }, ctx) => { - const comment = await ctx.mutators.Comment.create(input); - return { - edge: { - // 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, - }, - clientMutationId: input.clientMutationId, - }; - }, + createComment: async (source, { input }, ctx) => ({ + edge: { + // 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: await ctx.mutators.Comment.create(input), + }, + clientMutationId: input.clientMutationId, + }), + createCommentReply: async (source, { input }, ctx) => ({ + edge: { + // 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: await ctx.mutators.Comment.create(input), + }, + clientMutationId: input.clientMutationId, + }), updateSettings: async (source, { input }, ctx) => ({ settings: await ctx.mutators.Settings.update(input.settings), clientMutationId: input.clientMutationId, diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 94797b28f..f09176cc4 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -1529,11 +1529,6 @@ input CreateCommentInput { """ storyID: ID! - """ - parentID is the optional ID of the Comment that we are replying to. - """ - parentID: ID - """ body is the Comment body, the content of the Comment. """ @@ -1561,6 +1556,56 @@ type CreateCommentPayload { clientMutationId: String! } +################## +## createCommentReply +################## + +""" +CreateCommentReplyInput provides the input for the createCommentReply Mutation. +""" +input CreateCommentReplyInput { + """ + storyID is the ID of the Story where we are creating a comment on. + """ + storyID: ID! + + """ + parentID is the ID of the Comment that we are replying to. + """ + parentID: ID! + + """ + parentRevisionID is the ID of the CommentRevision that we are replying to. + """ + parentRevisionID: ID! + + """ + body is the Comment body, the content of the Comment. + """ + body: String! + + """ + clientMutationId is required for Relay support. + """ + clientMutationId: String! +} + +""" +CreateCommentReplyPayload contains the created Comment after the createCommentReply +mutation. +""" +type CreateCommentReplyPayload { + """ + edge is the possibly created comment edge. + """ + edge: CommentEdge + + """ + clientMutationId is required for Relay support. + """ + clientMutationId: String! +} + ################## ## editComment ################## @@ -2805,6 +2850,12 @@ type Mutation { """ createComment(input: CreateCommentInput!): CreateCommentPayload + """ + createCommentReply will create a Comment as the current logged in User that is + in reply to another Comment. + """ + createCommentReply(input: CreateCommentReplyInput!): CreateCommentReplyPayload + """ editComment will allow the author of a comment to change the body within the time allotment. diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index 405b15529..a30cf65c6 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -64,6 +64,12 @@ export interface Comment extends TenantResource { */ parentID?: string; + /** + * parentRevisionID is the ID of the Revision on the parent Comment that this + * was a reply to. + */ + parentRevisionID?: string; + /** * authorID stores the ID of the User that created this Comment. */