Skip to content

Commit

Permalink
Merge pull request #4615 from coralproject/develop
Browse files Browse the repository at this point in the history
v9.0.5
  • Loading branch information
tessalt committed May 13, 2024
2 parents af8ea5b + df36da8 commit efc7bb7
Show file tree
Hide file tree
Showing 25 changed files with 180 additions and 23 deletions.
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coralproject/talk",
"version": "9.0.4",
"version": "9.0.5",
"author": "The Coral Project",
"homepage": "https://coralproject.net/",
"sideEffects": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { graphql } from "relay-runtime";

import RecacheStoryAction from "coral-admin/components/StoryInfoDrawer/RecacheStoryAction";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { GQLSTORY_STATUS } from "coral-framework/schema";
import { GQLFEATURE_FLAG, GQLSTORY_STATUS } from "coral-framework/schema";
import { Flex, HorizontalGutter, TextLink } from "coral-ui/components/v2";
import ArchivedMarker from "coral-ui/components/v3/ArchivedMarker/ArchivedMarker";

Expand Down Expand Up @@ -32,6 +32,9 @@ const StoryInfoDrawerContainer: FunctionComponent<Props> = ({
viewer,
settings,
}) => {
const dataCacheEnabled = settings.featureFlags.includes(
GQLFEATURE_FLAG.DATA_CACHE
);
return (
<HorizontalGutter spacing={4} className={styles.root}>
<Flex justifyContent="flex-start">
Expand Down Expand Up @@ -84,13 +87,17 @@ const StoryInfoDrawerContainer: FunctionComponent<Props> = ({
<div className={styles.storyDrawerAction}>
<RescrapeStory storyID={story.id} />
</div>
<div className={styles.storyDrawerAction}>
<RecacheStoryAction storyID={story.id} />
</div>
{story.cached && (
<div className={styles.storyDrawerAction}>
<InvalidateCachedStoryAction storyID={story.id} />
</div>
{dataCacheEnabled && (
<>
<div className={styles.storyDrawerAction}>
<RecacheStoryAction storyID={story.id} />
</div>
{story.cached && (
<div className={styles.storyDrawerAction}>
<InvalidateCachedStoryAction storyID={story.id} />
</div>
)}
</>
)}
{viewer && (
<div className={styles.flexSizeToContentWidth}>
Expand Down Expand Up @@ -139,6 +146,7 @@ const enhanced = withFragmentContainer<Props>({
`,
settings: graphql`
fragment StoryInfoDrawerContainer_settings on Settings {
featureFlags
...ModerateStoryButton_settings
}
`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import PerspectiveConfig from "./PerspectiveConfig";
import PremoderateEmailAddressConfig from "./PremoderateEmailAddressConfig";
import PreModerationConfigContainer from "./PreModerationConfigContainer";
import RecentCommentHistoryConfig from "./RecentCommentHistoryConfig";
import UnmoderatedCountsConfig from "./UnmoderatedCountsConfig";

interface Props {
submitting: boolean;
Expand All @@ -46,6 +47,7 @@ export const ModerationConfigContainer: React.FunctionComponent<Props> = ({
<HorizontalGutter size="double" data-testid="configure-moderationContainer">
<PreModerationConfigContainer disabled={submitting} settings={settings} />
<PerspectiveConfig disabled={submitting} />
<UnmoderatedCountsConfig disabled={submitting} />
<AkismetConfig disabled={submitting} />
<NewCommentersConfigContainer disabled={submitting} settings={settings} />
<RecentCommentHistoryConfig disabled={submitting} />
Expand All @@ -61,6 +63,7 @@ const enhanced = withFragmentContainer<Props>({
fragment ModerationConfigContainer_settings on Settings {
...AkismetConfig_formValues @relay(mask: false)
...PerspectiveConfig_formValues @relay(mask: false)
...UnmoderatedCountsConfig_formValues @relay(mask: false)
...PreModerationConfigContainer_formValues @relay(mask: false)
...PreModerationConfigContainer_settings
...RecentCommentHistoryConfig_formValues @relay(mask: false)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Localized } from "@fluent/react/compat";
import React, { FunctionComponent } from "react";
import { graphql } from "react-relay";

import {
FieldSet,
FormField,
FormFieldHeader,
Label,
} from "coral-ui/components/v2";

import ConfigBox from "../../ConfigBox";
import Header from "../../Header";
import OnOffField from "../../OnOffField";

// eslint-disable-next-line no-unused-expressions
graphql`
fragment UnmoderatedCountsConfig_formValues on Settings {
showUnmoderatedCounts
}
`;

interface Props {
disabled: boolean;
}

const UnmoderatedCountsConfig: FunctionComponent<Props> = ({ disabled }) => {
return (
<ConfigBox
title={
<Localized id="configure-moderation-unmoderatedCounts-title">
<Header container={<legend />}>Unmoderated counts</Header>
</Localized>
}
container={<FieldSet />}
>
<FormField container={<FieldSet />}>
<FormFieldHeader>
<Localized id="configure-moderation-unmoderatedCounts-enabled">
<Label component="legend">
Show the number of unmoderated comments in the queue
</Label>
</Localized>
</FormFieldHeader>
<OnOffField name="showUnmoderatedCounts" disabled={disabled} />
</FormField>
</ConfigBox>
);
};

export default UnmoderatedCountsConfig;
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const ModerateNavigationContainer: React.FunctionComponent<Props> = (props) => {
section={props.section}
mode={props.settings?.moderation}
enableForReview={props.settings?.forReviewQueue}
showUnmoderatedCounts={props.settings?.showUnmoderatedCounts}
/>
);
};
Expand All @@ -91,6 +92,7 @@ const enhanced = withFragmentContainer<Props>({
fragment ModerateNavigationContainer_settings on Settings {
moderation
forReviewQueue
showUnmoderatedCounts
}
`,
moderationQueues: graphql`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface Props {
section?: SectionFilter | null;
mode?: "PRE" | "POST" | "SPECIFIC_SITES_PRE" | "%future added value" | null;
enableForReview?: boolean;
showUnmoderatedCounts?: boolean | null;
}

const Navigation: FunctionComponent<Props> = ({
Expand All @@ -42,6 +43,7 @@ const Navigation: FunctionComponent<Props> = ({
section,
mode,
enableForReview,
showUnmoderatedCounts,
}) => {
const { match, router } = useRouter();
const moderationLinks = useMemo(() => {
Expand Down Expand Up @@ -121,7 +123,7 @@ const Navigation: FunctionComponent<Props> = ({
<Localized id="moderate-navigation-unmoderated">
<span>Unmoderated</span>
</Localized>
{isNumber(unmoderatedCount) && (
{showUnmoderatedCounts && isNumber(unmoderatedCount) && (
<Counter data-testid="moderate-navigation-unmoderated-count">
<Localized
id="moderate-navigation-comment-count"
Expand Down
1 change: 1 addition & 0 deletions client/src/core/client/admin/test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ export const settings = createFixture<GQLSettings>({
},
},
protectedEmailDomains: Array.from(PROTECTED_EMAIL_DOMAINS),
showUnmoderatedCounts: true,
});

export const settingsWithMultisite = createFixture<GQLSettings>(
Expand Down
6 changes: 6 additions & 0 deletions common/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,4 +467,10 @@ export enum ERROR_CODES {
* to a DSA report that is too long
*/
DSA_REPORT_ADDITIONAL_INFO_TOO_LONG = "DSA_REPORT_ADDITIONAL_INFO_TOO_LONG",
/**
* UNABLE_TO_PRIME_CACHED_COMMENTS_FOR_STORY is thrown when DATA_CACHE is enabled and the
* priming of comments for the story in the data caches `commentCache` returns an undefined
* result. This usually means something went very wrong loading from Redis or Mongo.
*/
UNABLE_TO_PRIME_CACHED_COMMENTS_FOR_STORY = "UNABLE_TO_PRIME_CACHED_COMMENTS_FOR_STORY"
}
2 changes: 1 addition & 1 deletion common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "common",
"version": "9.0.4",
"version": "9.0.5",
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "common",
"version": "9.0.4",
"version": "9.0.5",
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
4 changes: 4 additions & 0 deletions locales/en-US/admin.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,10 @@ configure-moderation-newCommenters-approvedCommentsThreshold-description =
not have to be premoderated
configure-moderation-newCommenters-comments = comments
#### Unmoderated counts
configure-moderation-unmoderatedCounts-title = Unmoderated counts
configure-moderation-unmoderatedCounts-enabled = Show the number of unmoderated comments in the queue
#### Email domain
configure-moderation-emailDomains-header = Email domain
configure-moderation-emailDomains-description = Create rules to take action on accounts or comments based on the account holder's email address domain.
Expand Down
2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coralproject/talk",
"version": "9.0.4",
"version": "9.0.5",
"author": "The Coral Project",
"homepage": "https://coralproject.net/",
"sideEffects": [
Expand Down
12 changes: 12 additions & 0 deletions server/src/core/server/data/cache/commentCache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import RedisClient from "ioredis";
import { waitFor } from "coral-common/common/lib/helpers";
import { CommentCache } from "coral-server/data/cache/commentCache";
import { MongoContext, MongoContextImpl } from "coral-server/data/context";
import { UnableToPrimeCachedCommentsForStory } from "coral-server/errors";
import logger from "coral-server/logger";
import { Comment } from "coral-server/models/comment";
import { createMongoDB } from "coral-server/services/mongodb";
Expand Down Expand Up @@ -84,6 +85,10 @@ it("can load root comments from commentCache", async () => {
story.id,
false
);
if (!primeResult) {
throw new UnableToPrimeCachedCommentsForStory(story.tenantID, story.id);
}

const results = await comments.rootComments(
story.tenantID,
story.id,
Expand Down Expand Up @@ -144,6 +149,10 @@ it("can load replies from commentCache", async () => {
story.id,
false
);
if (!primeResult) {
throw new UnableToPrimeCachedCommentsForStory(story.tenantID, story.id);
}

const rootResults = await comments.rootComments(
story.tenantID,
story.id,
Expand Down Expand Up @@ -211,6 +220,9 @@ it("cache expires appropriately", async () => {
story.id,
false
);
if (!primeResult) {
throw new UnableToPrimeCachedCommentsForStory(story.tenantID, story.id);
}
expect(primeResult.retrievedFrom).toEqual("redis");

let lockExists = await redis.exists(lockKey);
Expand Down
10 changes: 10 additions & 0 deletions server/src/core/server/data/cache/commentCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface Filter {
}

export class CommentCache implements IDataCache {
public readonly primedStories: Set<string>;

private disableLocalCaching: boolean;
private expirySeconds: number;

Expand Down Expand Up @@ -55,6 +57,8 @@ export class CommentCache implements IDataCache {

this.commentsByKey = new Map<string, Readonly<Comment>>();
this.membersLookup = new Map<string, string[]>();

this.primedStories = new Set<string>();
}

public async available(tenantID: string): Promise<boolean> {
Expand Down Expand Up @@ -187,6 +191,10 @@ export class CommentCache implements IDataCache {
};
}

public shouldPrimeForStory(tenantID: string, storyID: string) {
return !this.primedStories.has(`${tenantID}:${storyID}`);
}

public async primeCommentsForStory(
tenantID: string,
storyID: string,
Expand Down Expand Up @@ -214,6 +222,8 @@ export class CommentCache implements IDataCache {
commentIDs.add(comment.id);
}

this.primedStories.add(`${tenantID}:${storyID}`);

return {
userIDs: Array.from(userIDs),
commentIDs: Array.from(commentIDs),
Expand Down
9 changes: 9 additions & 0 deletions server/src/core/server/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1064,3 +1064,12 @@ export class InvalidFlairBadgeName extends CoralError {
});
}
}

export class UnableToPrimeCachedCommentsForStory extends CoralError {
constructor(tenantID: string, storyID: string) {
super({
code: ERROR_CODES.UNABLE_TO_PRIME_CACHED_COMMENTS_FOR_STORY,
context: { pub: { tenantID } },
});
}
}
2 changes: 2 additions & 0 deletions server/src/core/server/errors/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,6 @@ export const ERROR_TRANSLATIONS: Record<ERROR_CODES, string> = {
INVALID_FLAIR_BADGE_NAME: "error-invalidFlairBadgeName",
DSA_REPORT_LAW_BROKEN_TOO_LONG: "error-dsaReportLawBrokenTooLong",
DSA_REPORT_ADDITIONAL_INFO_TOO_LONG: "error-dsaReportAdditionalInfoTooLong",
UNABLE_TO_PRIME_CACHED_COMMENTS_FOR_STORY:
"error-unableToPrimeCachedCommentsForStory",
};
12 changes: 10 additions & 2 deletions server/src/core/server/graph/loaders/Comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import DataLoader from "dataloader";
import { defaultTo, isNumber } from "lodash";
import { DateTime } from "luxon";

import { StoryNotFoundError } from "coral-server/errors";
import {
StoryNotFoundError,
UnableToPrimeCachedCommentsForStory,
} from "coral-server/errors";
import GraphContext from "coral-server/graph/context";
import { retrieveManyUserActionPresence } from "coral-server/models/action/comment";
import {
Expand Down Expand Up @@ -369,11 +372,16 @@ export default (ctx: GraphContext) => ({
return connection;
}

const { userIDs } = await ctx.cache.comments.primeCommentsForStory(
const primeResult = await ctx.cache.comments.primeCommentsForStory(
ctx.tenant.id,
storyID,
isArchived
);
if (!primeResult) {
throw new UnableToPrimeCachedCommentsForStory(ctx.tenant.id, storyID);
}

const { userIDs } = primeResult;
await ctx.cache.users.loadUsers(ctx.tenant.id, userIDs);
await ctx.cache.commentActions.primeCommentActions(ctx.tenant.id, story.id);

Expand Down
25 changes: 21 additions & 4 deletions server/src/core/server/graph/resolvers/Comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,29 @@ export const Comment: GQLCommentTypeResolver<comment.Comment> = {
},
statusHistory: ({ id }, input, ctx) =>
ctx.loaders.CommentModerationActions.forComment(input, id),
replies: (c, input, ctx) =>
replies: async (c, input, ctx) => {
// If there is at least one reply, then use the connection loader, otherwise
// return a blank connection.
c.childCount > 0
? ctx.loaders.Comments.forParent(c.storyID, c.id, input)
: createConnection(),
if (c.childCount === 0) {
return createConnection();
}

const cacheAvailable = await ctx.cache.available(ctx.tenant.id);
if (
cacheAvailable &&
ctx.cache.comments.shouldPrimeForStory(ctx.tenant.id, c.storyID)
) {
const story = await ctx.loaders.Stories.find.load({ id: c.storyID });
await ctx.cache.comments.primeCommentsForStory(
ctx.tenant.id,
c.storyID,
!!story?.isArchived
);
}

const result = await ctx.loaders.Comments.forParent(c.storyID, c.id, input);
return result;
},
replyCount: async ({ storyID, childIDs }, input, ctx) => {
// TODO: (wyattjoh) the childCount should be used eventually, but it should be managed with the status so it's only a count of published comments
if (childIDs.length === 0) {
Expand Down
2 changes: 2 additions & 0 deletions server/src/core/server/graph/resolvers/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,6 @@ export const Settings: GQLSettingsTypeResolver<Tenant> = {
inPageNotifications: ({
inPageNotifications = { enabled: true, floatingBellIndicator: true },
}) => inPageNotifications,
showUnmoderatedCounts: ({ showUnmoderatedCounts = true }) =>
showUnmoderatedCounts,
};

0 comments on commit efc7bb7

Please sign in to comment.