Skip to content

Commit efc7bb7

Browse files
authored
Merge pull request #4615 from coralproject/develop
v9.0.5
2 parents af8ea5b + df36da8 commit efc7bb7

File tree

25 files changed

+180
-23
lines changed

25 files changed

+180
-23
lines changed

client/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coralproject/talk",
3-
"version": "9.0.4",
3+
"version": "9.0.5",
44
"author": "The Coral Project",
55
"homepage": "https://coralproject.net/",
66
"sideEffects": [

client/src/core/client/admin/components/StoryInfoDrawer/StoryInfoDrawerContainer.tsx

+16-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { graphql } from "relay-runtime";
44

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

@@ -32,6 +32,9 @@ const StoryInfoDrawerContainer: FunctionComponent<Props> = ({
3232
viewer,
3333
settings,
3434
}) => {
35+
const dataCacheEnabled = settings.featureFlags.includes(
36+
GQLFEATURE_FLAG.DATA_CACHE
37+
);
3538
return (
3639
<HorizontalGutter spacing={4} className={styles.root}>
3740
<Flex justifyContent="flex-start">
@@ -84,13 +87,17 @@ const StoryInfoDrawerContainer: FunctionComponent<Props> = ({
8487
<div className={styles.storyDrawerAction}>
8588
<RescrapeStory storyID={story.id} />
8689
</div>
87-
<div className={styles.storyDrawerAction}>
88-
<RecacheStoryAction storyID={story.id} />
89-
</div>
90-
{story.cached && (
91-
<div className={styles.storyDrawerAction}>
92-
<InvalidateCachedStoryAction storyID={story.id} />
93-
</div>
90+
{dataCacheEnabled && (
91+
<>
92+
<div className={styles.storyDrawerAction}>
93+
<RecacheStoryAction storyID={story.id} />
94+
</div>
95+
{story.cached && (
96+
<div className={styles.storyDrawerAction}>
97+
<InvalidateCachedStoryAction storyID={story.id} />
98+
</div>
99+
)}
100+
</>
94101
)}
95102
{viewer && (
96103
<div className={styles.flexSizeToContentWidth}>
@@ -139,6 +146,7 @@ const enhanced = withFragmentContainer<Props>({
139146
`,
140147
settings: graphql`
141148
fragment StoryInfoDrawerContainer_settings on Settings {
149+
featureFlags
142150
...ModerateStoryButton_settings
143151
}
144152
`,

client/src/core/client/admin/routes/Configure/sections/Moderation/ModerationConfigContainer.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import PerspectiveConfig from "./PerspectiveConfig";
2020
import PremoderateEmailAddressConfig from "./PremoderateEmailAddressConfig";
2121
import PreModerationConfigContainer from "./PreModerationConfigContainer";
2222
import RecentCommentHistoryConfig from "./RecentCommentHistoryConfig";
23+
import UnmoderatedCountsConfig from "./UnmoderatedCountsConfig";
2324

2425
interface Props {
2526
submitting: boolean;
@@ -46,6 +47,7 @@ export const ModerationConfigContainer: React.FunctionComponent<Props> = ({
4647
<HorizontalGutter size="double" data-testid="configure-moderationContainer">
4748
<PreModerationConfigContainer disabled={submitting} settings={settings} />
4849
<PerspectiveConfig disabled={submitting} />
50+
<UnmoderatedCountsConfig disabled={submitting} />
4951
<AkismetConfig disabled={submitting} />
5052
<NewCommentersConfigContainer disabled={submitting} settings={settings} />
5153
<RecentCommentHistoryConfig disabled={submitting} />
@@ -61,6 +63,7 @@ const enhanced = withFragmentContainer<Props>({
6163
fragment ModerationConfigContainer_settings on Settings {
6264
...AkismetConfig_formValues @relay(mask: false)
6365
...PerspectiveConfig_formValues @relay(mask: false)
66+
...UnmoderatedCountsConfig_formValues @relay(mask: false)
6467
...PreModerationConfigContainer_formValues @relay(mask: false)
6568
...PreModerationConfigContainer_settings
6669
...RecentCommentHistoryConfig_formValues @relay(mask: false)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Localized } from "@fluent/react/compat";
2+
import React, { FunctionComponent } from "react";
3+
import { graphql } from "react-relay";
4+
5+
import {
6+
FieldSet,
7+
FormField,
8+
FormFieldHeader,
9+
Label,
10+
} from "coral-ui/components/v2";
11+
12+
import ConfigBox from "../../ConfigBox";
13+
import Header from "../../Header";
14+
import OnOffField from "../../OnOffField";
15+
16+
// eslint-disable-next-line no-unused-expressions
17+
graphql`
18+
fragment UnmoderatedCountsConfig_formValues on Settings {
19+
showUnmoderatedCounts
20+
}
21+
`;
22+
23+
interface Props {
24+
disabled: boolean;
25+
}
26+
27+
const UnmoderatedCountsConfig: FunctionComponent<Props> = ({ disabled }) => {
28+
return (
29+
<ConfigBox
30+
title={
31+
<Localized id="configure-moderation-unmoderatedCounts-title">
32+
<Header container={<legend />}>Unmoderated counts</Header>
33+
</Localized>
34+
}
35+
container={<FieldSet />}
36+
>
37+
<FormField container={<FieldSet />}>
38+
<FormFieldHeader>
39+
<Localized id="configure-moderation-unmoderatedCounts-enabled">
40+
<Label component="legend">
41+
Show the number of unmoderated comments in the queue
42+
</Label>
43+
</Localized>
44+
</FormFieldHeader>
45+
<OnOffField name="showUnmoderatedCounts" disabled={disabled} />
46+
</FormField>
47+
</ConfigBox>
48+
);
49+
};
50+
51+
export default UnmoderatedCountsConfig;

client/src/core/client/admin/routes/Moderate/ModerateNavigation/ModerateNavigationContainer.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ const ModerateNavigationContainer: React.FunctionComponent<Props> = (props) => {
7575
section={props.section}
7676
mode={props.settings?.moderation}
7777
enableForReview={props.settings?.forReviewQueue}
78+
showUnmoderatedCounts={props.settings?.showUnmoderatedCounts}
7879
/>
7980
);
8081
};
@@ -91,6 +92,7 @@ const enhanced = withFragmentContainer<Props>({
9192
fragment ModerateNavigationContainer_settings on Settings {
9293
moderation
9394
forReviewQueue
95+
showUnmoderatedCounts
9496
}
9597
`,
9698
moderationQueues: graphql`

client/src/core/client/admin/routes/Moderate/ModerateNavigation/Navigation.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface Props {
3131
section?: SectionFilter | null;
3232
mode?: "PRE" | "POST" | "SPECIFIC_SITES_PRE" | "%future added value" | null;
3333
enableForReview?: boolean;
34+
showUnmoderatedCounts?: boolean | null;
3435
}
3536

3637
const Navigation: FunctionComponent<Props> = ({
@@ -42,6 +43,7 @@ const Navigation: FunctionComponent<Props> = ({
4243
section,
4344
mode,
4445
enableForReview,
46+
showUnmoderatedCounts,
4547
}) => {
4648
const { match, router } = useRouter();
4749
const moderationLinks = useMemo(() => {
@@ -121,7 +123,7 @@ const Navigation: FunctionComponent<Props> = ({
121123
<Localized id="moderate-navigation-unmoderated">
122124
<span>Unmoderated</span>
123125
</Localized>
124-
{isNumber(unmoderatedCount) && (
126+
{showUnmoderatedCounts && isNumber(unmoderatedCount) && (
125127
<Counter data-testid="moderate-navigation-unmoderated-count">
126128
<Localized
127129
id="moderate-navigation-comment-count"

client/src/core/client/admin/test/fixtures.ts

+1
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ export const settings = createFixture<GQLSettings>({
242242
},
243243
},
244244
protectedEmailDomains: Array.from(PROTECTED_EMAIL_DOMAINS),
245+
showUnmoderatedCounts: true,
245246
});
246247

247248
export const settingsWithMultisite = createFixture<GQLSettings>(

common/lib/errors.ts

+6
Original file line numberDiff line numberDiff line change
@@ -467,4 +467,10 @@ export enum ERROR_CODES {
467467
* to a DSA report that is too long
468468
*/
469469
DSA_REPORT_ADDITIONAL_INFO_TOO_LONG = "DSA_REPORT_ADDITIONAL_INFO_TOO_LONG",
470+
/**
471+
* UNABLE_TO_PRIME_CACHED_COMMENTS_FOR_STORY is thrown when DATA_CACHE is enabled and the
472+
* priming of comments for the story in the data caches `commentCache` returns an undefined
473+
* result. This usually means something went very wrong loading from Redis or Mongo.
474+
*/
475+
UNABLE_TO_PRIME_CACHED_COMMENTS_FOR_STORY = "UNABLE_TO_PRIME_CACHED_COMMENTS_FOR_STORY"
470476
}

common/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "common",
3-
"version": "9.0.4",
3+
"version": "9.0.5",
44
"description": "",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

config/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "common",
3-
"version": "9.0.4",
3+
"version": "9.0.5",
44
"description": "",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

locales/en-US/admin.ftl

+4
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,10 @@ configure-moderation-newCommenters-approvedCommentsThreshold-description =
870870
not have to be premoderated
871871
configure-moderation-newCommenters-comments = comments
872872
873+
#### Unmoderated counts
874+
configure-moderation-unmoderatedCounts-title = Unmoderated counts
875+
configure-moderation-unmoderatedCounts-enabled = Show the number of unmoderated comments in the queue
876+
873877
#### Email domain
874878
configure-moderation-emailDomains-header = Email domain
875879
configure-moderation-emailDomains-description = Create rules to take action on accounts or comments based on the account holder's email address domain.

server/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coralproject/talk",
3-
"version": "9.0.4",
3+
"version": "9.0.5",
44
"author": "The Coral Project",
55
"homepage": "https://coralproject.net/",
66
"sideEffects": [

server/src/core/server/data/cache/commentCache.spec.ts

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import RedisClient from "ioredis";
33
import { waitFor } from "coral-common/common/lib/helpers";
44
import { CommentCache } from "coral-server/data/cache/commentCache";
55
import { MongoContext, MongoContextImpl } from "coral-server/data/context";
6+
import { UnableToPrimeCachedCommentsForStory } from "coral-server/errors";
67
import logger from "coral-server/logger";
78
import { Comment } from "coral-server/models/comment";
89
import { createMongoDB } from "coral-server/services/mongodb";
@@ -84,6 +85,10 @@ it("can load root comments from commentCache", async () => {
8485
story.id,
8586
false
8687
);
88+
if (!primeResult) {
89+
throw new UnableToPrimeCachedCommentsForStory(story.tenantID, story.id);
90+
}
91+
8792
const results = await comments.rootComments(
8893
story.tenantID,
8994
story.id,
@@ -144,6 +149,10 @@ it("can load replies from commentCache", async () => {
144149
story.id,
145150
false
146151
);
152+
if (!primeResult) {
153+
throw new UnableToPrimeCachedCommentsForStory(story.tenantID, story.id);
154+
}
155+
147156
const rootResults = await comments.rootComments(
148157
story.tenantID,
149158
story.id,
@@ -211,6 +220,9 @@ it("cache expires appropriately", async () => {
211220
story.id,
212221
false
213222
);
223+
if (!primeResult) {
224+
throw new UnableToPrimeCachedCommentsForStory(story.tenantID, story.id);
225+
}
214226
expect(primeResult.retrievedFrom).toEqual("redis");
215227

216228
let lockExists = await redis.exists(lockKey);

server/src/core/server/data/cache/commentCache.ts

+10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface Filter {
2525
}
2626

2727
export class CommentCache implements IDataCache {
28+
public readonly primedStories: Set<string>;
29+
2830
private disableLocalCaching: boolean;
2931
private expirySeconds: number;
3032

@@ -55,6 +57,8 @@ export class CommentCache implements IDataCache {
5557

5658
this.commentsByKey = new Map<string, Readonly<Comment>>();
5759
this.membersLookup = new Map<string, string[]>();
60+
61+
this.primedStories = new Set<string>();
5862
}
5963

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

194+
public shouldPrimeForStory(tenantID: string, storyID: string) {
195+
return !this.primedStories.has(`${tenantID}:${storyID}`);
196+
}
197+
190198
public async primeCommentsForStory(
191199
tenantID: string,
192200
storyID: string,
@@ -214,6 +222,8 @@ export class CommentCache implements IDataCache {
214222
commentIDs.add(comment.id);
215223
}
216224

225+
this.primedStories.add(`${tenantID}:${storyID}`);
226+
217227
return {
218228
userIDs: Array.from(userIDs),
219229
commentIDs: Array.from(commentIDs),

server/src/core/server/errors/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1064,3 +1064,12 @@ export class InvalidFlairBadgeName extends CoralError {
10641064
});
10651065
}
10661066
}
1067+
1068+
export class UnableToPrimeCachedCommentsForStory extends CoralError {
1069+
constructor(tenantID: string, storyID: string) {
1070+
super({
1071+
code: ERROR_CODES.UNABLE_TO_PRIME_CACHED_COMMENTS_FOR_STORY,
1072+
context: { pub: { tenantID } },
1073+
});
1074+
}
1075+
}

server/src/core/server/errors/translations.ts

+2
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,6 @@ export const ERROR_TRANSLATIONS: Record<ERROR_CODES, string> = {
8484
INVALID_FLAIR_BADGE_NAME: "error-invalidFlairBadgeName",
8585
DSA_REPORT_LAW_BROKEN_TOO_LONG: "error-dsaReportLawBrokenTooLong",
8686
DSA_REPORT_ADDITIONAL_INFO_TOO_LONG: "error-dsaReportAdditionalInfoTooLong",
87+
UNABLE_TO_PRIME_CACHED_COMMENTS_FOR_STORY:
88+
"error-unableToPrimeCachedCommentsForStory",
8789
};

server/src/core/server/graph/loaders/Comments.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import DataLoader from "dataloader";
22
import { defaultTo, isNumber } from "lodash";
33
import { DateTime } from "luxon";
44

5-
import { StoryNotFoundError } from "coral-server/errors";
5+
import {
6+
StoryNotFoundError,
7+
UnableToPrimeCachedCommentsForStory,
8+
} from "coral-server/errors";
69
import GraphContext from "coral-server/graph/context";
710
import { retrieveManyUserActionPresence } from "coral-server/models/action/comment";
811
import {
@@ -369,11 +372,16 @@ export default (ctx: GraphContext) => ({
369372
return connection;
370373
}
371374

372-
const { userIDs } = await ctx.cache.comments.primeCommentsForStory(
375+
const primeResult = await ctx.cache.comments.primeCommentsForStory(
373376
ctx.tenant.id,
374377
storyID,
375378
isArchived
376379
);
380+
if (!primeResult) {
381+
throw new UnableToPrimeCachedCommentsForStory(ctx.tenant.id, storyID);
382+
}
383+
384+
const { userIDs } = primeResult;
377385
await ctx.cache.users.loadUsers(ctx.tenant.id, userIDs);
378386
await ctx.cache.commentActions.primeCommentActions(ctx.tenant.id, story.id);
379387

server/src/core/server/graph/resolvers/Comment.ts

+21-4
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,29 @@ export const Comment: GQLCommentTypeResolver<comment.Comment> = {
147147
},
148148
statusHistory: ({ id }, input, ctx) =>
149149
ctx.loaders.CommentModerationActions.forComment(input, id),
150-
replies: (c, input, ctx) =>
150+
replies: async (c, input, ctx) => {
151151
// If there is at least one reply, then use the connection loader, otherwise
152152
// return a blank connection.
153-
c.childCount > 0
154-
? ctx.loaders.Comments.forParent(c.storyID, c.id, input)
155-
: createConnection(),
153+
if (c.childCount === 0) {
154+
return createConnection();
155+
}
156+
157+
const cacheAvailable = await ctx.cache.available(ctx.tenant.id);
158+
if (
159+
cacheAvailable &&
160+
ctx.cache.comments.shouldPrimeForStory(ctx.tenant.id, c.storyID)
161+
) {
162+
const story = await ctx.loaders.Stories.find.load({ id: c.storyID });
163+
await ctx.cache.comments.primeCommentsForStory(
164+
ctx.tenant.id,
165+
c.storyID,
166+
!!story?.isArchived
167+
);
168+
}
169+
170+
const result = await ctx.loaders.Comments.forParent(c.storyID, c.id, input);
171+
return result;
172+
},
156173
replyCount: async ({ storyID, childIDs }, input, ctx) => {
157174
// 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
158175
if (childIDs.length === 0) {

server/src/core/server/graph/resolvers/Settings.ts

+2
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,6 @@ export const Settings: GQLSettingsTypeResolver<Tenant> = {
8787
inPageNotifications: ({
8888
inPageNotifications = { enabled: true, floatingBellIndicator: true },
8989
}) => inPageNotifications,
90+
showUnmoderatedCounts: ({ showUnmoderatedCounts = true }) =>
91+
showUnmoderatedCounts,
9092
};

0 commit comments

Comments
 (0)