[CORL-181] Comment Count Injection (#2581)

* feat: inject comment counts

* fix: tests

* feat: integrate stream embed with coral counts

* chore: refactor constants

* test: test for count bundle

* test: test live comment count integration with stream embed

* feat: use defer

* fix: snapshot

* feat: auto add count.js in when calling Coral.createStreamEmbed

* fix: tests

* fix: rm duplicate test

* chore: remove unuse file
This commit is contained in:
Vinh
2019-10-01 19:22:15 +00:00
committed by Wyatt Johnson
parent 53fa5f43e5
commit 808b355a27
48 changed files with 888 additions and 50 deletions
@@ -5,3 +5,4 @@ export * from "./health";
export * from "./install";
export * from "./version";
export * from "./user";
export * from "./story";
@@ -0,0 +1,71 @@
import { RequestHandler } from "coral-server/types/express";
import createDOMPurify from "dompurify";
import { JSDOM } from "jsdom";
import { AppOptions } from "coral-server/app";
import { calculateTotalPublishedCommentCount } from "coral-server/models/story";
import { translate } from "coral-server/services/i18n";
import { find } from "coral-server/services/stories";
const NUMBER_CLASSNAME = "coral-count-number";
const TEXT_CLASSNAME = "coral-count-text";
export type CountOptions = Pick<AppOptions, "mongo" | "tenantCache" | "i18n">;
/**
* countHandler returns translated comment counts using JSONP.
*/
export const countHandler = ({ mongo, i18n }: CountOptions): RequestHandler => {
const window = new JSDOM("").window;
const DOMPurify = createDOMPurify(window);
return async (req, res, next) => {
try {
// Tenant is guaranteed at this point.
const coral = req.coral!;
const tenant = coral.tenant!;
const story = await find(mongo, tenant, {
id: req.query.id,
url: req.query.url,
});
if (!story) {
throw new Error("Story not found");
}
const count = calculateTotalPublishedCommentCount(
story.commentCounts.status
);
let html = "";
if (req.query.notext === "true") {
// We only need the count without the text.
html = `<span class="${NUMBER_CLASSNAME}">${count}</span>`;
} else {
// Use translated string.
const bundle = i18n.getBundle(tenant.locale);
html = translate(
bundle,
`<span class="${NUMBER_CLASSNAME}">${count}</span> <span class="${TEXT_CLASSNAME}">Comments</span>`,
"comment-count",
{
number: count,
numberClass: NUMBER_CLASSNAME,
textClass: TEXT_CLASSNAME,
}
);
// Strip dangerous html from translation.
html = DOMPurify.sanitize(html) as string;
}
// Respond using jsonp.
res.jsonp({
// Reference from the client that we'll just send back as it is.
ref: req.query.ref,
html,
});
} catch (err) {
return next(err);
}
};
};
@@ -0,0 +1 @@
export * from "./count";
+2
View File
@@ -18,6 +18,7 @@ import { tenantMiddleware } from "coral-server/app/middleware/tenant";
import { createNewAccountRouter } from "./account";
import { createNewAuthRouter } from "./auth";
import { createStoryRouter } from "./story";
import { createNewUserRouter } from "./user";
export interface RouterOptions {
@@ -57,6 +58,7 @@ export function createAPIRouter(app: AppOptions, options: RouterOptions) {
router.use("/auth", createNewAuthRouter(app, options));
router.use("/account", createNewAccountRouter(app, options));
router.use("/user", createNewUserRouter(app));
router.use("/story", createStoryRouter(app));
// Configure the GraphQL route.
router.use(
+13
View File
@@ -0,0 +1,13 @@
import express from "express";
import { AppOptions } from "coral-server/app";
import { countHandler } from "coral-server/app/handlers";
import { cacheHeadersMiddleware } from "coral-server/app/middleware/cacheHeaders";
export function createStoryRouter(app: AppOptions) {
const router = express.Router();
// TODO: (cvle) make caching time configurable?
router.get("/count.js", cacheHeadersMiddleware("2m"), countHandler(app));
return router;
}
@@ -1,14 +1,14 @@
import { GQLCommentCountsTypeResolver } from "coral-server/graph/tenant/schema/__generated__/types";
import { PUBLISHED_STATUSES } from "coral-server/models/comment/constants";
import { Story } from "coral-server/models/story";
import {
calculateTotalPublishedCommentCount,
Story,
} from "coral-server/models/story";
export type CommentCountsInput = Pick<Story, "commentCounts" | "id">;
export const CommentCounts: GQLCommentCountsTypeResolver<CommentCountsInput> = {
totalPublished: ({ commentCounts }) =>
PUBLISHED_STATUSES.reduce(
(total, status) => total + commentCounts.status[status],
0
),
calculateTotalPublishedCommentCount(commentCounts.status),
statuses: ({ commentCounts }) => commentCounts.status,
tags: (s, input, ctx) => ctx.loaders.Comments.tagCounts.load(s.id),
};
+7
View File
@@ -4,3 +4,10 @@ disableCommentingDefaultMessage = Comments are closed on this story.
reaction-labelRespect = Respect
reaction-labelActiveRespected = Respected
reaction-sortLabelMostRespected = Most Respected
comment-count =
<span class="{ $numberClass }">{ $number }</span>
<span class="{ $textClass }">{ $number ->
[one] Comment
*[other] Comments
}</span>
+2 -4
View File
@@ -3,6 +3,7 @@ import {
GQLTAG,
} from "coral-server/graph/tenant/schema/__generated__/types";
import { calculateTotalPublishedCommentCount } from "../story";
import { Comment } from "./comment";
import { MODERATOR_STATUSES, PUBLISHED_STATUSES } from "./constants";
import { Revision } from "./revision";
@@ -68,10 +69,7 @@ export function createEmptyCommentStatusCounts(): CommentStatusCounts {
}
export function calculateRejectionRate(counts: CommentStatusCounts): number {
const published = PUBLISHED_STATUSES.reduce(
(acc, status) => counts[status] + acc,
0
);
const published = calculateTotalPublishedCommentCount(counts);
const rejected = counts[GQLCOMMENT_STATUS.REJECTED];
return rejected / (published + rejected);
@@ -9,6 +9,7 @@ import { dotize } from "coral-common/utils/dotize";
import { GQLCOMMENT_STATUS } from "coral-server/graph/tenant/schema/__generated__/types";
import logger from "coral-server/logger";
import { EncodedCommentActionCounts } from "coral-server/models/action/comment";
import { PUBLISHED_STATUSES } from "coral-server/models/comment/constants";
import {
CommentStatusCounts,
createEmptyCommentStatusCounts,
@@ -226,3 +227,16 @@ export function calculateTotalCommentCount(
}
return count;
}
/**
* calculateTotalPublishedCommentCount will compute the total amount of
* published comments in a story by parsing the `CommentStatusCounts`.
*/
export function calculateTotalPublishedCommentCount(
commentCounts: CommentStatusCounts
) {
return PUBLISHED_STATUSES.reduce(
(total, status) => total + commentCounts[status],
0
);
}