From b3064c89deeea0c66354c81fef8cb167cd962e32 Mon Sep 17 00:00:00 2001 From: Tessa Thornton Date: Fri, 20 Sep 2019 16:11:05 -0400 Subject: [PATCH] [CORL-623] add live updates for comments moving from premod to approved (#2571) * add live updates for comments moving from premod to approved * rename disposables * check if comment is system withheld as well as premod to publish event --- .../AllCommentsTabContainer.tsx | 14 ++- .../CommentReleasedSubscription.tsx | 91 +++++++++++++++++++ .../resolvers/CommentReleasedPayload.ts | 11 +++ .../resolvers/Subscription/commentReleased.ts | 30 ++++++ .../tenant/resolvers/Subscription/index.ts | 2 + .../tenant/resolvers/Subscription/types.ts | 5 +- .../server/graph/tenant/resolvers/index.ts | 2 + .../server/graph/tenant/schema/schema.graphql | 15 +++ .../services/comments/moderation/index.ts | 9 ++ src/core/server/services/events/comments.ts | 15 +++ 10 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 src/core/client/stream/tabs/Comments/Stream/AllCommentsTab/CommentReleasedSubscription.tsx create mode 100644 src/core/server/graph/tenant/resolvers/CommentReleasedPayload.ts create mode 100644 src/core/server/graph/tenant/resolvers/Subscription/commentReleased.ts diff --git a/src/core/client/stream/tabs/Comments/Stream/AllCommentsTab/AllCommentsTabContainer.tsx b/src/core/client/stream/tabs/Comments/Stream/AllCommentsTab/AllCommentsTabContainer.tsx index 6453c48e3..7189bff48 100644 --- a/src/core/client/stream/tabs/Comments/Stream/AllCommentsTab/AllCommentsTabContainer.tsx +++ b/src/core/client/stream/tabs/Comments/Stream/AllCommentsTab/AllCommentsTabContainer.tsx @@ -25,6 +25,7 @@ import IgnoredTombstoneOrHideContainer from "../../IgnoredTombstoneOrHideContain import { ReplyListContainer } from "../../ReplyList"; import AllCommentsTabViewNewMutation from "./AllCommentsTabViewNewMutation"; import CommentCreatedSubscription from "./CommentCreatedSubscription"; +import CommentReleasedSubscription from "./CommentReleasedSubscription"; import styles from "./AllCommentsTabContainer.css"; @@ -54,6 +55,9 @@ export const AllCommentsTabContainer: FunctionComponent = props => { ` ); const subscribeToCommentCreated = useSubscription(CommentCreatedSubscription); + const subscribeToCommentReleased = useSubscription( + CommentReleasedSubscription + ); useEffect(() => { if (!props.story.settings.live.enabled) { return; @@ -78,16 +82,22 @@ export const AllCommentsTabContainer: FunctionComponent = props => { // Only chronological sort supports top level live updates of incoming comments. return; } - const disposable = subscribeToCommentCreated({ + const newCommentDisposable = subscribeToCommentCreated({ + storyID: props.story.id, + orderBy: commentsOrderBy, + }); + const releasedCommentDisposable = subscribeToCommentReleased({ storyID: props.story.id, orderBy: commentsOrderBy, }); return () => { - disposable.dispose(); + newCommentDisposable.dispose(); + releasedCommentDisposable.dispose(); }; }, [ commentsOrderBy, subscribeToCommentCreated, + subscribeToCommentReleased, props.story.id, props.relay.hasMore(), props.story.settings.live.enabled, diff --git a/src/core/client/stream/tabs/Comments/Stream/AllCommentsTab/CommentReleasedSubscription.tsx b/src/core/client/stream/tabs/Comments/Stream/AllCommentsTab/CommentReleasedSubscription.tsx new file mode 100644 index 000000000..62aa449cd --- /dev/null +++ b/src/core/client/stream/tabs/Comments/Stream/AllCommentsTab/CommentReleasedSubscription.tsx @@ -0,0 +1,91 @@ +import { graphql, requestSubscription } from "react-relay"; +import { + ConnectionHandler, + Environment, + RecordProxy, + RecordSourceSelectorProxy, +} from "relay-runtime"; + +import { + createSubscription, + SubscriptionVariables, +} from "coral-framework/lib/relay"; +import { GQLCOMMENT_SORT, GQLCOMMENT_SORT_RL } from "coral-framework/schema"; +import { CommentReleasedSubscription } from "coral-stream/__generated__/CommentReleasedSubscription.graphql"; + +function updateForNewestFirst( + store: RecordSourceSelectorProxy, + storyID: string +) { + const rootField = store.getRootField("commentReleased"); + if (!rootField) { + return; + } + const comment = rootField.getLinkedRecord("comment")!; + comment.setValue(true, "enteredLive"); + const commentsEdge = store.create( + `edge-${comment.getValue("id")!}`, + "CommentsEdge" + ); + commentsEdge.setValue(comment.getValue("createdAt"), "cursor"); + commentsEdge.setLinkedRecord(comment, "node"); + const story = store.get(storyID)!; + const connection = ConnectionHandler.getConnection(story, "Stream_comments", { + orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC, + })!; + const linked = connection.getLinkedRecords("viewNewEdges") || []; + connection.setLinkedRecords(linked.concat(commentsEdge), "viewNewEdges"); +} + +function updateForOldestFirst( + store: RecordSourceSelectorProxy, + storyID: string +) { + const story = store.get(storyID)!; + const connection = ConnectionHandler.getConnection(story, "Stream_comments", { + orderBy: GQLCOMMENT_SORT.CREATED_AT_ASC, + })!; + const pageInfo = connection.getLinkedRecord("pageInfo") as RecordProxy; + pageInfo.setValue(true, "hasNextPage"); +} + +const CommentReleasedSubscription = createSubscription( + "subscribeToCommentReleased", + ( + environment: Environment, + variables: SubscriptionVariables & { + orderBy: GQLCOMMENT_SORT_RL; + } + ) => + requestSubscription(environment, { + subscription: graphql` + subscription CommentReleasedSubscription($storyID: ID!) { + commentReleased(storyID: $storyID) { + comment { + id + createdAt + ...AllCommentsTabContainer_comment + } + } + } + `, + variables, + updater: store => { + if (variables.orderBy === GQLCOMMENT_SORT.CREATED_AT_DESC) { + updateForNewestFirst(store, variables.storyID); + return; + } + if (variables.orderBy === GQLCOMMENT_SORT.CREATED_AT_ASC) { + updateForOldestFirst(store, variables.storyID); + return; + } + throw new Error( + `Unsupport new top level comment live updates for sort ${ + variables.orderBy + }` + ); + }, + }) +); + +export default CommentReleasedSubscription; diff --git a/src/core/server/graph/tenant/resolvers/CommentReleasedPayload.ts b/src/core/server/graph/tenant/resolvers/CommentReleasedPayload.ts new file mode 100644 index 000000000..8ccd84300 --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/CommentReleasedPayload.ts @@ -0,0 +1,11 @@ +import { GQLCommentReleasedPayloadTypeResolver } from "coral-server/graph/tenant/schema/__generated__/types"; + +import { maybeLoadOnlyID } from "./Comment"; +import { CommentReleasedInput } from "./Subscription/commentReleased"; + +export const CommentReleasedPayload: GQLCommentReleasedPayloadTypeResolver< + CommentReleasedInput +> = { + comment: ({ commentID }, args, ctx, info) => + maybeLoadOnlyID(ctx, info, commentID), +}; diff --git a/src/core/server/graph/tenant/resolvers/Subscription/commentReleased.ts b/src/core/server/graph/tenant/resolvers/Subscription/commentReleased.ts new file mode 100644 index 000000000..4e420ccea --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/Subscription/commentReleased.ts @@ -0,0 +1,30 @@ +import { SubscriptionToCommentReleasedResolver } from "coral-server/graph/tenant/schema/__generated__/types"; + +import { createIterator } from "./helpers"; +import { + SUBSCRIPTION_CHANNELS, + SubscriptionPayload, + SubscriptionType, +} from "./types"; + +export interface CommentReleasedInput extends SubscriptionPayload { + storyID: string; + commentID: string; +} + +export type CommentReleasedSubscription = SubscriptionType< + SUBSCRIPTION_CHANNELS.COMMENT_RELEASED, + CommentReleasedInput +>; + +export const commentReleased: SubscriptionToCommentReleasedResolver< + CommentReleasedInput +> = createIterator(SUBSCRIPTION_CHANNELS.COMMENT_RELEASED, { + filter: (source, { storyID }) => { + if (source.storyID !== storyID) { + return false; + } + + return true; + }, +}); diff --git a/src/core/server/graph/tenant/resolvers/Subscription/index.ts b/src/core/server/graph/tenant/resolvers/Subscription/index.ts index bbd7d7b2a..e7283dc09 100644 --- a/src/core/server/graph/tenant/resolvers/Subscription/index.ts +++ b/src/core/server/graph/tenant/resolvers/Subscription/index.ts @@ -4,6 +4,7 @@ import { commentCreated } from "./commentCreated"; import { commentEnteredModerationQueue } from "./commentEnteredModerationQueue"; import { commentFeatured } from "./commentFeatured"; import { commentLeftModerationQueue } from "./commentLeftModerationQueue"; +import { commentReleased } from "./commentReleased"; import { commentReplyCreated } from "./commentReplyCreated"; import { commentStatusUpdated } from "./commentStatusUpdated"; @@ -14,4 +15,5 @@ export const Subscription: GQLSubscriptionTypeResolver = { commentReplyCreated, commentStatusUpdated, commentFeatured, + commentReleased, }; diff --git a/src/core/server/graph/tenant/resolvers/Subscription/types.ts b/src/core/server/graph/tenant/resolvers/Subscription/types.ts index 21d9e2017..72c44904d 100644 --- a/src/core/server/graph/tenant/resolvers/Subscription/types.ts +++ b/src/core/server/graph/tenant/resolvers/Subscription/types.ts @@ -2,6 +2,7 @@ import { CommentCreatedSubscription } from "./commentCreated"; import { CommentEnteredModerationQueueSubscription } from "./commentEnteredModerationQueue"; import { CommentFeaturedSubscription } from "./commentFeatured"; import { CommentLeftModerationQueueSubscription } from "./commentLeftModerationQueue"; +import { CommentReleasedSubscription } from "./commentReleased"; import { CommentReplyCreatedSubscription } from "./commentReplyCreated"; import { CommentStatusUpdatedSubscription } from "./commentStatusUpdated"; @@ -12,6 +13,7 @@ export enum SUBSCRIPTION_CHANNELS { COMMENT_REPLY_CREATED = "COMMENT_REPLY_CREATED", COMMENT_CREATED = "COMMENT_CREATED", COMMENT_FEATURED = "COMMENT_FEATURED", + COMMENT_RELEASED = "COMMENT_RELEASED", } export interface SubscriptionPayload { @@ -32,4 +34,5 @@ export type SUBSCRIPTION_INPUT = | CommentStatusUpdatedSubscription | CommentReplyCreatedSubscription | CommentCreatedSubscription - | CommentFeaturedSubscription; + | CommentFeaturedSubscription + | CommentReleasedSubscription; diff --git a/src/core/server/graph/tenant/resolvers/index.ts b/src/core/server/graph/tenant/resolvers/index.ts index a99ff1c72..8404e5beb 100644 --- a/src/core/server/graph/tenant/resolvers/index.ts +++ b/src/core/server/graph/tenant/resolvers/index.ts @@ -14,6 +14,7 @@ import { CommentCreatedPayload } from "./CommentCreatedPayload"; import { CommentEnteredModerationQueuePayload } from "./CommentEnteredModerationQueuePayload"; import { CommentLeftModerationQueuePayload } from "./CommentLeftModerationQueuePayload"; import { CommentModerationAction } from "./CommentModerationAction"; +import { CommentReleasedPayload } from "./CommentReleasedPayload"; import { CommentReplyCreatedPayload } from "./CommentReplyCreatedPayload"; import { CommentRevision } from "./CommentRevision"; import { CommentStatusUpdatedPayload } from "./CommentStatusUpdatedPayload"; @@ -52,6 +53,7 @@ const Resolvers: GQLResolver = { Comment, CommentCounts, CommentCreatedPayload, + CommentReleasedPayload, CommentEnteredModerationQueuePayload, CommentLeftModerationQueuePayload, CommentModerationAction, diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index b024ff80e..674369f16 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -5321,6 +5321,16 @@ type CommentFeaturedPayload { comment: Comment! } +""" +CommentReleasedPayload is returned when a new top level Comment is approved from premod stream +""" +type CommentReleasedPayload { + """ + comment is the new top level Comment that was approved on the Story. + """ + comment: Comment! +} + type Subscription { """ commentEnteredModerationQueue returns when a Comment enters a ModerationQueue. @@ -5353,6 +5363,11 @@ type Subscription { """ commentCreated(storyID: ID!): CommentCreatedPayload! + """ + commentReleased returns when a Comment on a premoderated stream is approved + """ + commentReleased(storyID: ID!): CommentReleasedPayload! + """ commentReplyCreated returns when a Comment is posted in the ancestor chain of comments. diff --git a/src/core/server/services/comments/moderation/index.ts b/src/core/server/services/comments/moderation/index.ts index 4e5ac0c05..7922689a0 100644 --- a/src/core/server/services/comments/moderation/index.ts +++ b/src/core/server/services/comments/moderation/index.ts @@ -13,6 +13,7 @@ import { updateCommentStatus } from "coral-server/models/comment"; import { updateStoryCounts } from "coral-server/models/story"; import { Tenant } from "coral-server/models/tenant"; import { + publishCommentReleased, publishCommentStatusChanges, publishModerationQueueChanges, } from "coral-server/services/events"; @@ -114,6 +115,14 @@ const moderate = ( result.comment.id, input.moderatorID ); + if ( + [GQLCOMMENT_STATUS.PREMOD, GQLCOMMENT_STATUS.SYSTEM_WITHHELD].includes( + result.oldStatus + ) && + status === "APPROVED" + ) { + publishCommentReleased(publish, result.comment); + } log.trace({ oldStatus: result.oldStatus }, "adjusted story comment counts"); diff --git a/src/core/server/services/events/comments.ts b/src/core/server/services/events/comments.ts index cfebdfde6..4ebdff5e0 100644 --- a/src/core/server/services/events/comments.ts +++ b/src/core/server/services/events/comments.ts @@ -57,6 +57,21 @@ export function publishCommentCreated( } } +export function publishCommentReleased( + publish: Publisher, + comment: Pick +) { + if (!comment.parentID && hasPublishedStatus(comment)) { + publish({ + channel: SUBSCRIPTION_CHANNELS.COMMENT_RELEASED, + payload: { + commentID: comment.id, + storyID: comment.storyID, + }, + }); + } +} + export function publishCommentFeatured( publish: Publisher, comment: Pick