diff --git a/src/core/client/stream/tabs/comments/containers/RespectButtonContainer.tsx b/src/core/client/stream/tabs/comments/containers/RespectButtonContainer.tsx index e247fb8ac..2da162157 100644 --- a/src/core/client/stream/tabs/comments/containers/RespectButtonContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/RespectButtonContainer.tsx @@ -45,7 +45,6 @@ class RespectButtonContainer extends React.Component< this.props.comment.myActionPresence && this.props.comment.myActionPresence.reaction; - console.log(totalReactions); return ( = { // TODO: implement this. isClosed: () => false, actionCounts: asset => decodeActionCounts(asset.action_counts), + commentCounts: asset => asset.comment_counts, }; export default Asset; diff --git a/src/core/server/graph/tenant/resolvers/comment_counts.ts b/src/core/server/graph/tenant/resolvers/comment_counts.ts new file mode 100644 index 000000000..b88a54c3b --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/comment_counts.ts @@ -0,0 +1,14 @@ +import { + GQLCOMMENT_STATUS, + GQLCommentCountsTypeResolver, +} from "talk-server/graph/tenant/schema/__generated__/types"; +import { CommentStatusCounts } from "talk-server/models/asset"; + +const CommentCounts: GQLCommentCountsTypeResolver = { + totalVisible: commentCounts => + commentCounts[GQLCOMMENT_STATUS.ACCEPTED] + + commentCounts[GQLCOMMENT_STATUS.NONE], + statuses: commentCounts => commentCounts, +}; + +export default CommentCounts; diff --git a/src/core/server/graph/tenant/resolvers/index.ts b/src/core/server/graph/tenant/resolvers/index.ts index f8754d04b..e1bf08143 100644 --- a/src/core/server/graph/tenant/resolvers/index.ts +++ b/src/core/server/graph/tenant/resolvers/index.ts @@ -4,6 +4,7 @@ import { GQLResolver } from "talk-server/graph/tenant/schema/__generated__/types import Asset from "./asset"; import AuthIntegrations from "./auth_integrations"; import Comment from "./comment"; +import CommentCounts from "./comment_counts"; import Mutation from "./mutation"; import Profile from "./profile"; import Query from "./query"; @@ -12,6 +13,7 @@ const Resolvers: GQLResolver = { Asset, AuthIntegrations, Comment, + CommentCounts, Cursor, Mutation, Profile, diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index c2c2467a0..63b82301a 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -423,7 +423,7 @@ type KarmaThreshold { type KarmaThresholds { """ flag represents karma settings in relation to how well a User's flagging - ability aligns with the moderation decicions made by moderators. + ability aligns with the moderation decisions made by moderators. """ flag: KarmaThreshold! @@ -442,7 +442,7 @@ type Karma { """ karmaThresholds contains the currently set thresholds for triggering Trust - beheviour. + behavior. """ thresholds: KarmaThresholds! } @@ -935,6 +935,50 @@ type CommentsConnection { pageInfo: PageInfo! } +################################################################################ +## CommentCounts +################################################################################ + +type CommentStatusCounts { + """ + The comment is not PREMOD, but was not applied a moderation status by a + moderator. + """ + NONE: Int! + + """ + The comment has been accepted by a moderator. + """ + ACCEPTED: Int! + + """ + The comment has been rejected by a moderator. + """ + REJECTED: Int! + + """ + The comment was created while the asset's premoderation option was on, and + new comments that haven't been moderated yet are referred to as + "premoderated" or "premod" comments. + """ + PREMOD: Int! + + """ + SYSTEM_WITHHELD represents a comment that was withheld by the system because + it was flagged by an internal process for further review. + """ + SYSTEM_WITHHELD: Int! +} + +type CommentCounts { + """ + totalVisible will return the count of all visible Comments. + """ + totalVisible: Int! + + statuses: CommentStatusCounts! @auth(roles: [ADMIN, MODERATOR]) +} + ################################################################################ ## Asset ################################################################################ @@ -995,6 +1039,11 @@ type Asset { """ isClosed: Boolean! + """ + commentCounts stores all the counts of Comments that are left on the Comment. + """ + commentCounts: CommentCounts! + """ createdAt is the date that the Asset was created at. """ @@ -1319,7 +1368,7 @@ input SettingsKarmaThresholdInput { input SettingsKarmaThresholdsInput { """ flag represents karma settings in relation to how well a User's flagging - ability aligns with the moderation decicions made by moderators. + ability aligns with the moderation decisions made by moderators. """ flag: SettingsKarmaThresholdInput @@ -1338,7 +1387,7 @@ input SettingsKarmaInput { """ karmaThresholds contains the currently set thresholds for triggering Trust - beheviour. + behavior. """ thresholds: SettingsKarmaThresholdsInput } diff --git a/src/core/server/models/asset.ts b/src/core/server/models/asset.ts index aa3289503..8c5dc4856 100644 --- a/src/core/server/models/asset.ts +++ b/src/core/server/models/asset.ts @@ -3,6 +3,7 @@ import uuid from "uuid"; import { Omit } from "talk-common/types"; import { dotize } from "talk-common/utils/dotize"; +import { GQLCOMMENT_STATUS } from "talk-server/graph/tenant/schema/__generated__/types"; import { EncodedActionCounts } from "talk-server/models/action"; import { ModerationSettings } from "talk-server/models/settings"; import { TenantResource } from "talk-server/models/tenant"; @@ -11,12 +12,24 @@ function collection(db: Db) { return db.collection>("assets"); } +// TODO: (wyattjoh) write a test to verify that this set of counts is always in sync with GQLCOMMENT_STATUS. +export interface CommentStatusCounts { + [GQLCOMMENT_STATUS.ACCEPTED]: number; + [GQLCOMMENT_STATUS.NONE]: number; + [GQLCOMMENT_STATUS.PREMOD]: number; + [GQLCOMMENT_STATUS.REJECTED]: number; + [GQLCOMMENT_STATUS.SYSTEM_WITHHELD]: number; +} + export interface Asset extends TenantResource { readonly id: string; url: string; scraped?: Date; closedAt?: Date; closedMessage?: string; + created_at: Date; + modified_date?: Date; + title?: string; description?: string; image?: string; @@ -24,8 +37,6 @@ export interface Asset extends TenantResource { subsection?: string; author?: string; publication_date?: Date; - modified_date?: Date; - created_at: Date; /** * action_counts stores all the action counts for all Comment's on this Asset. @@ -33,7 +44,13 @@ export interface Asset extends TenantResource { action_counts: EncodedActionCounts; /** - * settings provides a point where the settings can be overriden for a + * comment_counts stores the different counts for each comment on the Asset + * according to their statuses. + */ + comment_counts: CommentStatusCounts; + + /** + * settings provides a point where the settings can be overridden for a * specific Asset. */ settings?: Partial; @@ -62,13 +79,17 @@ export async function upsertAsset( tenant_id: tenantID, created_at: now, action_counts: {}, + comment_counts: createEmptyCommentCounts(), }, }; // Perform the find and update operation to try and find and or create the // asset. const { value: asset } = await collection(db).findOneAndUpdate( - { url }, + { + url, + tenant_id: tenantID, + }, update, { // Create the object if it doesn't already exist. @@ -90,6 +111,48 @@ export async function upsertAsset( return asset; } +/** + * updateCommentStatusCount increments the number of status counts for the + * given Asset ID. + * + * @param mongo the database handle + * @param tenantID the tenant that the Asset is on. + * @param id the ID of the Asset. + * @param commentStatusCounts the update document that contains a positive or + * negative number of comments to increment on the given Asset. + */ +export async function updateCommentStatusCount( + mongo: Db, + tenantID: string, + id: string, + commentStatusCounts: Partial +) { + const { value: asset } = await collection(mongo).findOneAndUpdate( + { + id, + tenant_id: tenantID, + }, + // Update all the specific comment status counts that are associated with + // each of the counts. + { $inc: dotize({ comment_counts: commentStatusCounts }) }, + // False to return the updated document instead of the original + // document. + { returnOriginal: false } + ); + + return asset; +} + +function createEmptyCommentCounts(): CommentStatusCounts { + return { + [GQLCOMMENT_STATUS.ACCEPTED]: 0, + [GQLCOMMENT_STATUS.NONE]: 0, + [GQLCOMMENT_STATUS.PREMOD]: 0, + [GQLCOMMENT_STATUS.REJECTED]: 0, + [GQLCOMMENT_STATUS.SYSTEM_WITHHELD]: 0, + }; +} + export interface FindOrCreateAssetInput { id?: string; url?: string; diff --git a/src/core/server/services/comments/index.ts b/src/core/server/services/comments/index.ts index 85813d3b6..82e10c97c 100644 --- a/src/core/server/services/comments/index.ts +++ b/src/core/server/services/comments/index.ts @@ -2,7 +2,10 @@ import { Db } from "mongodb"; import { Omit } from "talk-common/types"; import { ACTION_ITEM_TYPE, CreateActionInput } from "talk-server/models/action"; -import { retrieveAsset } from "talk-server/models/asset"; +import { + retrieveAsset, + updateCommentStatusCount, +} from "talk-server/models/asset"; import { createComment, CreateCommentInput, @@ -102,6 +105,11 @@ export async function create( ); } + // Increment the status count for the particular status on the Asset. + await updateCommentStatusCount(mongo, tenant.id, asset.id, { + [status]: 1, + }); + return comment; } @@ -118,7 +126,7 @@ export async function edit( req?: Request ) { // Get the comment that we're editing. - let comment = await retrieveComment(mongo, tenant.id, input.id); + const comment = await retrieveComment(mongo, tenant.id, input.id); if (!comment) { // TODO: replace to match error returned by the models/comments.ts throw new Error("comment not found"); @@ -140,7 +148,7 @@ export async function edit( req, }); - comment = await editComment(mongo, tenant.id, { + let editedComment = await editComment(mongo, tenant.id, { id: input.id, author_id: author.id, body: input.body, @@ -174,8 +182,17 @@ export async function edit( ); // Insert and handle creating the actions. - comment = await addCommentActions(mongo, tenant, comment, inputs); + editedComment = await addCommentActions(mongo, tenant, comment, inputs); } - return comment; + if (comment.status !== editedComment.status) { + // Increment the status count for the particular status on the Asset, and + // decrement the status on the comment's previous status. + await updateCommentStatusCount(mongo, tenant.id, asset.id, { + [comment.status]: -1, + [editedComment.status]: 1, + }); + } + + return editedComment; }