mirror of
https://github.com/wassname/talk.git
synced 2026-08-07 11:29:44 +08:00
[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:
@@ -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";
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user