diff --git a/client/package-lock.json b/client/package-lock.json index 29c95a9626..ccc5ad1e4f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "@coralproject/talk", - "version": "8.5.1", + "version": "8.5.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@coralproject/talk", - "version": "8.5.1", + "version": "8.5.2", "license": "Apache-2.0", "dependencies": { "@ampproject/toolbox-cache-url": "^2.9.0", diff --git a/client/package.json b/client/package.json index ec70cda48f..c5524d9029 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@coralproject/talk", - "version": "8.5.1", + "version": "8.5.2", "author": "The Coral Project", "homepage": "https://coralproject.net/", "sideEffects": [ diff --git a/client/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx b/client/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx index 73b1a6d764..4c89753386 100644 --- a/client/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/Advanced/AdvancedConfigContainer.tsx @@ -13,7 +13,7 @@ import { AdvancedConfigContainer_settings } from "coral-admin/__generated__/Adva import AMPConfig from "./AMPConfig"; import CommentStreamLiveUpdatesContainer from "./CommentStreamLiveUpdatesContainer"; import CustomCSSConfig from "./CustomCSSConfig"; -import EmbeddedCommentRepliesConfig from "./EmbeddedCommentRepliesConfig"; +import EmbeddedCommentsConfig from "./EmbeddedCommentsConfig"; import ForReviewQueueConfig from "./ForReviewQueueConfig"; import StoryCreationConfig from "./StoryCreationConfig"; @@ -31,7 +31,7 @@ const AdvancedConfigContainer: React.FunctionComponent = ({ return ( - + ({ settings: graphql` fragment AdvancedConfigContainer_settings on Settings { ...CustomCSSConfig_formValues @relay(mask: false) - ...EmbeddedCommentRepliesConfig_formValues @relay(mask: false) + ...EmbeddedCommentsConfig_formValues @relay(mask: false) ...CommentStreamLiveUpdates_formValues @relay(mask: false) ...StoryCreationConfig_formValues @relay(mask: false) ...CommentStreamLiveUpdatesContainer_settings diff --git a/client/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentRepliesConfig.tsx b/client/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentRepliesConfig.tsx deleted file mode 100644 index f78b905743..0000000000 --- a/client/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentRepliesConfig.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Localized } from "@fluent/react/compat"; -import React, { FunctionComponent } from "react"; -import { graphql } from "react-relay"; - -import { FormField, FormFieldDescription, 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 EmbeddedCommentRepliesConfig_formValues on Settings { - embeddedComments { - allowReplies - } - } -`; - -interface Props { - disabled: boolean; -} - -const EmbeddedCommentRepliesConfig: FunctionComponent = ({ - disabled, -}) => ( - -
- Embedded comment replies -
- - } - > - - - - When enabled, a reply button will appear with each embedded comment to - encourage additional discussion on that specific comment or story. - - - - - - - -
-); - -export default EmbeddedCommentRepliesConfig; diff --git a/client/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx b/client/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx new file mode 100644 index 0000000000..a22d78332b --- /dev/null +++ b/client/src/core/client/admin/routes/Configure/sections/Advanced/EmbeddedCommentsConfig.tsx @@ -0,0 +1,72 @@ +import { Localized } from "@fluent/react/compat"; +import React, { FunctionComponent } from "react"; +import { graphql } from "react-relay"; + +import { FormField, HelperText, Label } from "coral-ui/components/v2"; + +import ConfigBox from "../../ConfigBox"; +import Header from "../../Header"; +import OnOffField from "../../OnOffField"; +import Subheader from "../../Subheader"; +import AllowedOriginsTextarea from "../Sites/AllowedOriginsTextarea"; + +// eslint-disable-next-line no-unused-expressions +graphql` + fragment EmbeddedCommentsConfig_formValues on Settings { + embeddedComments { + allowReplies + oEmbedAllowedOrigins + } + } +`; + +interface Props { + disabled: boolean; +} + +const EmbeddedCommentsConfig: FunctionComponent = ({ disabled }) => ( + +
+ Embedded comments +
+ + } + > + + + + + + + When enabled, a reply button will appear with each embedded comment to + encourage additional discussion on that specific comment or story. + + + + + + For sites using oEmbed + + + + + + + + Domains that are permitted to make calls to the oEmbed API (ex. + http://localhost:3000, https://staging.domain.com, + https://domain.com). + + + + +
+); + +export default EmbeddedCommentsConfig; diff --git a/client/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx b/client/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx index 3c513fe16d..b0735ad4b1 100644 --- a/client/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/Sites/AllowedOriginsTextarea.tsx @@ -10,12 +10,18 @@ import ValidationMessage from "../../ValidationMessage"; import styles from "./AllowedOriginsTextarea.css"; interface Props { + name: string; defaultValue?: ReadonlyArray; + disabled?: boolean; } -const AllowedOriginsTextarea: FunctionComponent = ({ defaultValue }) => ( +const AllowedOriginsTextarea: FunctionComponent = ({ + name, + defaultValue, + disabled = false, +}) => ( = ({ defaultValue }) => ( autoCapitalize="off" spellCheck={false} fullwidth + disabled={disabled} /> diff --git a/client/src/core/client/admin/routes/Configure/sections/Sites/CreateSiteForm.tsx b/client/src/core/client/admin/routes/Configure/sections/Sites/CreateSiteForm.tsx index 1cbf98d254..4651f9f4d0 100644 --- a/client/src/core/client/admin/routes/Configure/sections/Sites/CreateSiteForm.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/Sites/CreateSiteForm.tsx @@ -84,7 +84,7 @@ const CreateSiteForm: FunctionComponent = ({ onCreate }) => { - + {submitError && ( diff --git a/client/src/core/client/admin/routes/Configure/sections/Sites/EditSiteForm.tsx b/client/src/core/client/admin/routes/Configure/sections/Sites/EditSiteForm.tsx index 4461581f88..ffb1f455de 100644 --- a/client/src/core/client/admin/routes/Configure/sections/Sites/EditSiteForm.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/Sites/EditSiteForm.tsx @@ -90,7 +90,10 @@ const EditSiteForm: FunctionComponent = ({ - + diff --git a/client/src/core/client/admin/test/configure/advanced.spec.tsx b/client/src/core/client/admin/test/configure/advanced.spec.tsx index ed95c5f900..05a7050be3 100644 --- a/client/src/core/client/admin/test/configure/advanced.spec.tsx +++ b/client/src/core/client/admin/test/configure/advanced.spec.tsx @@ -59,7 +59,7 @@ it("renders configure advanced", async () => { const { configureContainer } = await createTestRenderer(); expect(within(configureContainer).getByLabelText("Custom CSS")).toBeDefined(); expect( - within(configureContainer).getByText("Embedded comment replies") + within(configureContainer).getByText("Embedded comments") ).toBeDefined(); expect( within(configureContainer).getByText("Comment stream live updates") @@ -223,7 +223,7 @@ it("change embedded comments allow replies", async () => { }); const embeddedCommentReplies = within(advancedContainer).getByTestId( - "embedded-comment-replies-config" + "embedded-comments-config" ); const offField = within(embeddedCommentReplies).getByText("Off"); @@ -241,3 +241,43 @@ it("change embedded comments allow replies", async () => { expect(resolvers.Mutation!.updateSettings!.called).toBe(true); }); }); + +it("change oembed permitted domains", async () => { + const resolvers = createResolversStub({ + Mutation: { + updateSettings: ({ variables }) => { + expectAndFail( + variables.settings.embeddedComments?.oEmbedAllowedOrigins + ).toEqual(["http://localhost:8080"]); + return { + settings: pureMerge(settings, variables.settings), + }; + }, + }, + }); + const { advancedContainer, saveChangesButton } = await createTestRenderer({ + resolvers, + }); + + const oembedAllowedOriginsConfig = within(advancedContainer).getByTestId( + "embedded-comments-config" + ); + + const allowedOriginsTextArea = within(oembedAllowedOriginsConfig).getByRole( + "textbox" + ); + + userEvent.type(allowedOriginsTextArea, "http://"); + + userEvent.click(saveChangesButton); + + expect(within(advancedContainer).getByText("Invalid URL")); + + userEvent.type(allowedOriginsTextArea, "localhost:8080"); + + userEvent.click(saveChangesButton); + + await waitFor(() => { + expect(resolvers.Mutation!.updateSettings!.called).toBe(true); + }); +}); diff --git a/client/src/core/client/admin/test/fixtures.ts b/client/src/core/client/admin/test/fixtures.ts index 9a3ddf61b1..4c6f4d7a19 100644 --- a/client/src/core/client/admin/test/fixtures.ts +++ b/client/src/core/client/admin/test/fixtures.ts @@ -224,6 +224,7 @@ export const settings = createFixture({ emailDomainModeration: [], embeddedComments: { allowReplies: true, + oEmbedAllowedOrigins: [], }, flairBadges: { flairBadgesEnabled: false, diff --git a/client/src/core/client/stream/common/scrollToBeginning.ts b/client/src/core/client/stream/common/scrollToBeginning.ts index f173fa3612..0171f660fb 100644 --- a/client/src/core/client/stream/common/scrollToBeginning.ts +++ b/client/src/core/client/stream/common/scrollToBeginning.ts @@ -12,6 +12,8 @@ function scrollToBeginning( } else { window.scrollTo({ top: getElementWindowTopOffset(window, tab) }); } + // set keyboard focus to Comments button for accessibility + tab.getElementsByTagName("button")[0].focus(); } } diff --git a/client/src/core/client/stream/tabs/Comments/Comment/CommentToggle.tsx b/client/src/core/client/stream/tabs/Comments/Comment/CommentToggle.tsx index a4333d3075..1561332a8b 100644 --- a/client/src/core/client/stream/tabs/Comments/Comment/CommentToggle.tsx +++ b/client/src/core/client/stream/tabs/Comments/Comment/CommentToggle.tsx @@ -40,6 +40,7 @@ const CommentToggle: FunctionComponent = (props) => { onClick={props.toggleCollapsed} className={cn(styles.root, CLASSES.comment.collapseToggle.$root)} aria-label={"Expand comment thread"} + aria-expanded="false" > = ({ styles.toggleButton, CLASSES.comment.collapseToggle.$root )} + aria-expanded="true" > = ({ onClick={onToggleExpand} size="small" className={styles.button} + aria-expanded="false" > {media.__typename === "TwitterMedia" && ( @@ -129,6 +130,7 @@ const MediaSectionContainer: FunctionComponent = ({ size="small" iconLeft className={styles.button} + aria-expanded="true" > = ({ const permalinkUrl = getURLWithCommentID(story.url, comment.id); - const embedCode = `
{ if (customScrollContainer) { customScrollContainer.scrollTo({ top: 0 }); } renderWindow.scrollTo({ top: 0 }); - }, [renderWindow, customScrollContainer]); + // programmatically apply focus to first keyboard focusable element + // after scroll for accessibility + const firstKeyboardFocusableElement = getFirstKeyboardFocusableElement(); + if (firstKeyboardFocusableElement instanceof HTMLElement) { + firstKeyboardFocusableElement.focus(); + } + }, [renderWindow, customScrollContainer, getFirstKeyboardFocusableElement]); const onGoToCommentsTop = useCallback(() => { scrollToBeginning(root, renderWindow, customScrollContainer); }, [root, renderWindow, customScrollContainer]); diff --git a/client/src/core/client/stream/tabs/Profile/Preferences/IgnoreUserSettingsContainer.tsx b/client/src/core/client/stream/tabs/Profile/Preferences/IgnoreUserSettingsContainer.tsx index a724eb3088..f26932b43b 100644 --- a/client/src/core/client/stream/tabs/Profile/Preferences/IgnoreUserSettingsContainer.tsx +++ b/client/src/core/client/stream/tabs/Profile/Preferences/IgnoreUserSettingsContainer.tsx @@ -82,6 +82,7 @@ const IgnoreUserSettingsContainer: FunctionComponent = ({ viewer }) => { upperCase onClick={toggleManage} className={CLASSES.ignoredCommenters.manageButton} + aria-expanded="true" > Close @@ -103,6 +104,7 @@ const IgnoreUserSettingsContainer: FunctionComponent = ({ viewer }) => { CLASSES.ignoredCommenters.manageButton )} aria-label="Manage ignored commenters" + aria-expanded="false" > Manage diff --git a/common/package-lock.json b/common/package-lock.json index 0e94ee3ed7..c72c76c8cc 100644 --- a/common/package-lock.json +++ b/common/package-lock.json @@ -1,12 +1,12 @@ { "name": "common", - "version": "8.5.1", + "version": "8.5.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "common", - "version": "8.5.1", + "version": "8.5.2", "license": "ISC", "dependencies": { "coral-config": "../config/dist", diff --git a/common/package.json b/common/package.json index 67709e9a0a..e32ab18b77 100644 --- a/common/package.json +++ b/common/package.json @@ -1,6 +1,6 @@ { "name": "common", - "version": "8.5.1", + "version": "8.5.2", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/config/package-lock.json b/config/package-lock.json index f5c43b71aa..c698d123ba 100644 --- a/config/package-lock.json +++ b/config/package-lock.json @@ -1,12 +1,12 @@ { "name": "common", - "version": "8.5.1", + "version": "8.5.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "common", - "version": "8.5.1", + "version": "8.5.2", "license": "ISC", "dependencies": { "typescript": "^3.9.5" diff --git a/config/package.json b/config/package.json index bda0dd5a2f..258ab18151 100644 --- a/config/package.json +++ b/config/package.json @@ -1,6 +1,6 @@ { "name": "common", - "version": "8.5.1", + "version": "8.5.2", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/docs/docs/counts.md b/docs/docs/counts.md index 9be2eebcdf..1109bc09dd 100644 --- a/docs/docs/counts.md +++ b/docs/docs/counts.md @@ -37,8 +37,7 @@ After successful injection it will become: Set the class of your html element to `coral-count` in order to get story counts. The following `data-coral-*` attributes will configure the output: - `data-coral-id` – The id of the story of which counts should be injected. -- `data-coral-url` – The URL of the story of which counts should be injected. +- `data-coral-url` – The URL of the story of which counts should be injected. See more info below on when it needs to be set. - `data-coral-notext` – If set to `"true"`, only the count number will be injected -Either `data-coral-id` or `data-coral-url` should be set. If none are provided the story URL -will be retrieved from the [canonical url reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Choosing_between_www_and_non-www_URLs#Using_%3Clink_relcanonical%3E) `` or inferred using the current page url. +The `data-coral-url` attribute should always be provided or be able to be inferred from the canonical url reference or current page url. If it is not provided in the data attribute, the story URL will be retrieved from the [canonical url reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Choosing_between_www_and_non-www_URLs#Using_%3Clink_relcanonical%3E) `` or inferred using the current page url. Therefore, on pages where the story URL cannot be inferred from the canonical url reference or current page url (such as indexes and homepages), it's important that the `data-coral-url` be included. diff --git a/docs/docs/migrating-6-to-7.md b/docs/docs/migrating-6-to-7.md index 553090043e..c59799668b 100644 --- a/docs/docs/migrating-6-to-7.md +++ b/docs/docs/migrating-6-to-7.md @@ -57,6 +57,14 @@ html { 4. If your site has a Content Security Policy that prohibits websocket (`wss://`) requests, you will need to update it to whitelist `wss://[your coral URL]/api/graphql/live` in order for live updates to function. +## Native mobile applications + +- If the Coral embed is not loading in web view after this update, you may need to add in this call: + +```js +webView.getSettings.setDomStorageEnabled(true); +``` + ## After Coral has been updated to v7 Once the update is complete and you are running v7, you may wish to undo some of the previous steps for better maintainability: diff --git a/docs/docs/mobile.md b/docs/docs/mobile.md index 3647c52b89..fb535b9abd 100644 --- a/docs/docs/mobile.md +++ b/docs/docs/mobile.md @@ -29,3 +29,11 @@ Integration with native mobile applications is done through web view, you will n This will initialize the Coral stream with a logged-in user. 5. You will need to use the same method to pass a Story ID or Story URL to the embed code using the `storyID` or `storyURL` options passed to `createStreamEmbed`. See [CMS Integration](/cms) for more details. + +### Troubleshooting + +If the Coral embed is not loading in web view, and you are using Coral >= v7.0.0, you may need to add in this call: + +```js +webView.getSettings.setDomStorageEnabled(true); +``` diff --git a/locales/en-US/admin.ftl b/locales/en-US/admin.ftl index 1ba67c3b5e..33246960de 100644 --- a/locales/en-US/admin.ftl +++ b/locales/en-US/admin.ftl @@ -905,12 +905,17 @@ configure-advanced-customCSS-containsFontFace = URL to a custom CSS stylesheet that contains all @font-face definitions needed by above stylesheet. -configure-advanced-embeddedCommentReplies = Embedded comment replies +configure-advanced-embeddedComments = Embedded comments +configure-advanced-embeddedComments-subheader = For sites using oEmbed configure-advanced-embeddedCommentReplies-explanation = When enabled, a reply button will appear with each embedded comment to encourage additional discussion on that specific comment or story. configure-advanced-embeddedCommentReplies-label = Allow replies to embedded comments +configure-advanced-oembedAllowedOrigins-header = oEmbed permitted domains +configure-advanced-oembedAllowedOrigins-description = Domains that are permitted to make calls to the oEmbed API (ex. http://localhost:3000, https://staging.domain.com, https://domain.com). +configure-advanced-oembedAllowedOrigins-label = oEmbed permitted domains + configure-advanced-permittedDomains = Permitted domains configure-advanced-permittedDomains-description = Domains where your { -product-name } instance is allowed to be embedded diff --git a/locales/en-US/stream.ftl b/locales/en-US/stream.ftl index d9df263453..6aa895eae6 100644 --- a/locales/en-US/stream.ftl +++ b/locales/en-US/stream.ftl @@ -450,9 +450,9 @@ comments-featured-label = Featured Comment from {$username} comments-featured-gotoConversation = Go to conversation comments-featured-gotoConversation-label-with-username = - .aria-label = Go to this featured comment by user { $username } in the main comment stream + .aria-label = Go to conversation for this featured comment by user { $username } in the main comment stream comments-featured-gotoConversation-label-without-username = - .aria-label = Go to this featured comment in the main comment stream + .aria-label = Go to conversation for this featured comment in the main comment stream comments-featured-replies = Replies ## Profile Tab diff --git a/server/package-lock.json b/server/package-lock.json index ed165afa9f..ab9f9954e2 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@coralproject/talk", - "version": "8.5.1", + "version": "8.5.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@coralproject/talk", - "version": "8.5.1", + "version": "8.5.2", "license": "Apache-2.0", "dependencies": { "@ampproject/toolbox-cache-url": "^2.9.0", diff --git a/server/package.json b/server/package.json index 5374b676c7..96ea8f47d7 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@coralproject/talk", - "version": "8.5.1", + "version": "8.5.2", "author": "The Coral Project", "homepage": "https://coralproject.net/", "sideEffects": [ diff --git a/server/src/core/server/app/handlers/api/story/active.ts b/server/src/core/server/app/handlers/api/story/active.ts index bf0ba6b44b..f6575d3b17 100644 --- a/server/src/core/server/app/handlers/api/story/active.ts +++ b/server/src/core/server/app/handlers/api/story/active.ts @@ -14,11 +14,13 @@ export type Options = Pick; const ActiveStoriesQuerySchema = Joi.object().keys({ callback: Joi.string().allow("").optional(), siteID: Joi.string().required(), + count: Joi.number().optional().max(999), }); interface ActiveStoriesQuery { callback: string; siteID: string; + count: number; } /** @@ -44,7 +46,7 @@ export const activeJSONPHandler = const { tenant, now } = req.coral; // Ensure we have a siteID on the query. - const { siteID }: ActiveStoriesQuery = validate( + const { siteID, count }: ActiveStoriesQuery = validate( ActiveStoriesQuerySchema, req.query ); @@ -61,7 +63,7 @@ export const activeJSONPHandler = mongo, tenant.id, siteID, - 5, + count ?? 5, start, now ); diff --git a/server/src/core/server/app/handlers/api/story/count.ts b/server/src/core/server/app/handlers/api/story/count.ts index 2a30d906f6..9356df22b6 100644 --- a/server/src/core/server/app/handlers/api/story/count.ts +++ b/server/src/core/server/app/handlers/api/story/count.ts @@ -39,7 +39,7 @@ interface StoryCountJSONPQuery { ref: string; } -function getTextHTML( +export function getTextHTML( tenant: Readonly, storyMode: GQLSTORY_MODE | undefined | null, i18n: I18n, diff --git a/server/src/core/server/app/middleware/commentEmbedWhitelisted.ts b/server/src/core/server/app/middleware/commentEmbedWhitelisted.ts index 0ccb3732e7..0f6fd29f1f 100644 --- a/server/src/core/server/app/middleware/commentEmbedWhitelisted.ts +++ b/server/src/core/server/app/middleware/commentEmbedWhitelisted.ts @@ -9,7 +9,7 @@ import { AppOptions } from ".."; import { getRequesterOrigin } from "../helpers"; export const commentEmbedWhitelisted = - ({ mongo }: Pick): RequestHandler => + ({ mongo }: Pick, oembedAPI = false): RequestHandler => async (req, res, next) => { // First try to get the commentID from the query params let { commentID } = req.query; @@ -37,6 +37,13 @@ export const commentEmbedWhitelisted = origin = req.header("Origin"); } if (origin) { + // if oEmbed API call, we also check oEmbed allowed origins on tenant + if ( + oembedAPI && + tenant.embeddedComments?.oEmbedAllowedOrigins.includes(origin) + ) { + return next(); + } if (site.allowedOrigins.includes(origin)) { return next(); } diff --git a/server/src/core/server/app/router/api/index.ts b/server/src/core/server/app/router/api/index.ts index 34bc5793c8..afe215a9eb 100644 --- a/server/src/core/server/app/router/api/index.ts +++ b/server/src/core/server/app/router/api/index.ts @@ -97,7 +97,7 @@ export function createAPIRouter(app: AppOptions, options: RouterOptions) { router.get("/oembed", cspSiteMiddleware(app), oembedHandler(app)); router.get( "/services/oembed", - commentEmbedWhitelisted(app), + commentEmbedWhitelisted(app, true), cors(createCommentEmbedCorsOptionsDelegate(app.mongo)), oembedProviderHandler(app) ); diff --git a/server/src/core/server/cron/accountDeletion.ts b/server/src/core/server/cron/accountDeletion.ts index 3da626d01b..362fa92114 100644 --- a/server/src/core/server/cron/accountDeletion.ts +++ b/server/src/core/server/cron/accountDeletion.ts @@ -2,6 +2,7 @@ import { Config } from "coral-server/config"; import { MongoContext } from "coral-server/data/context"; import { retrieveLockedUserScheduledForDeletion } from "coral-server/models/user"; import { MailerQueue } from "coral-server/queue/tasks/mailer"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { TenantCache } from "coral-server/services/tenant/cache"; import { deleteUser } from "coral-server/services/users/delete"; @@ -16,6 +17,7 @@ interface Options { mongo: MongoContext; redis: AugmentedRedis; config: Config; + i18n: I18n; mailerQueue: MailerQueue; tenantCache: TenantCache; } @@ -39,6 +41,7 @@ const deleteScheduledAccounts: ScheduledJobCommand = async ({ mongo, redis, config, + i18n, mailerQueue, tenantCache, }) => { @@ -64,7 +67,7 @@ const deleteScheduledAccounts: ScheduledJobCommand = async ({ log.info({ userID: user.id }, "deleting user"); - await deleteUser(mongo, redis, config, user.id, tenant.id, now); + await deleteUser(mongo, redis, config, i18n, user.id, tenant.id, now); // If the user has an email, then send them a confirmation that their account // was deleted. diff --git a/server/src/core/server/cron/index.ts b/server/src/core/server/cron/index.ts index fa5c99d706..7013c4ca95 100644 --- a/server/src/core/server/cron/index.ts +++ b/server/src/core/server/cron/index.ts @@ -2,6 +2,7 @@ import { Config } from "coral-server/config"; import { MongoContext } from "coral-server/data/context"; import { ArchiverQueue } from "coral-server/queue/tasks/archiver"; import { MailerQueue } from "coral-server/queue/tasks/mailer"; +import { I18n } from "coral-server/services/i18n"; import { JWTSigningConfig } from "coral-server/services/jwt"; import { AugmentedRedis } from "coral-server/services/redis"; import { TenantCache } from "coral-server/services/tenant/cache"; @@ -20,6 +21,7 @@ interface Options { mongo: MongoContext; redis: AugmentedRedis; config: Config; + i18n: I18n; mailerQueue: MailerQueue; archiverQueue: ArchiverQueue; signingConfig: JWTSigningConfig; diff --git a/server/src/core/server/graph/mutators/Actions.ts b/server/src/core/server/graph/mutators/Actions.ts index 5a8c6757ba..9e30accd15 100644 --- a/server/src/core/server/graph/mutators/Actions.ts +++ b/server/src/core/server/graph/mutators/Actions.ts @@ -20,6 +20,7 @@ export const Actions = (ctx: GraphContext) => ({ ctx.redis, ctx.cache, ctx.config, + ctx.i18n, ctx.broker, ctx.tenant, input.commentID, @@ -38,6 +39,7 @@ export const Actions = (ctx: GraphContext) => ({ ctx.redis, ctx.cache, ctx.config, + ctx.i18n, ctx.broker, ctx.tenant, input.commentID, diff --git a/server/src/core/server/graph/mutators/Comments.ts b/server/src/core/server/graph/mutators/Comments.ts index 0b7e4a9fbd..b49dde6fbc 100644 --- a/server/src/core/server/graph/mutators/Comments.ts +++ b/server/src/core/server/graph/mutators/Comments.ts @@ -54,6 +54,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.wordList, ctx.cache, ctx.config, + ctx.i18n, ctx.broker, ctx.tenant, ctx.user!, @@ -84,6 +85,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.wordList, ctx.cache, ctx.config, + ctx.i18n, ctx.broker, ctx.tenant, ctx.user!, @@ -111,6 +113,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.mongo, ctx.redis, ctx.config, + ctx.i18n, ctx.cache, ctx.broker, ctx.tenant, @@ -129,6 +132,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.mongo, ctx.redis, ctx.config, + ctx.i18n, ctx.cache, ctx.broker, ctx.tenant, @@ -147,6 +151,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.mongo, ctx.redis, ctx.config, + ctx.i18n, ctx.cache.commentActions, ctx.broker, ctx.tenant, @@ -170,6 +175,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.mongo, ctx.redis, ctx.config, + ctx.i18n, ctx.cache, ctx.broker, ctx.tenant, @@ -189,6 +195,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.mongo, ctx.redis, ctx.config, + ctx.i18n, ctx.cache.commentActions, ctx.broker, ctx.tenant, @@ -229,6 +236,7 @@ export const Comments = (ctx: GraphContext) => ({ ctx.redis, ctx.cache, ctx.config, + ctx.i18n, ctx.broker, ctx.tenant, commentID, diff --git a/server/src/core/server/graph/mutators/Users.ts b/server/src/core/server/graph/mutators/Users.ts index 89c9f54309..c107a13e9c 100644 --- a/server/src/core/server/graph/mutators/Users.ts +++ b/server/src/core/server/graph/mutators/Users.ts @@ -185,6 +185,7 @@ export const Users = (ctx: GraphContext) => ({ ctx.mongo, ctx.redis, ctx.config, + ctx.i18n, input.userID, ctx.tenant.id, ctx.now diff --git a/server/src/core/server/graph/resolvers/Settings.ts b/server/src/core/server/graph/resolvers/Settings.ts index 4c922a80ca..7988ef3b09 100644 --- a/server/src/core/server/graph/resolvers/Settings.ts +++ b/server/src/core/server/graph/resolvers/Settings.ts @@ -54,10 +54,15 @@ export const Settings: GQLSettingsTypeResolver = { return deprecated; }, embeddedComments: ( - { embeddedComments = { allowReplies: true } }, + { embeddedComments = { allowReplies: true, oEmbedAllowedOrigins: [] } }, args, ctx - ) => embeddedComments, + ) => { + return { + allowReplies: embeddedComments.allowReplies ?? true, + oEmbedAllowedOrigins: embeddedComments.oEmbedAllowedOrigins ?? [], + }; + }, flairBadges: ({ flairBadges = { flairBadgesEnabled: false, badges: [] }, }) => { diff --git a/server/src/core/server/graph/schema/schema.graphql b/server/src/core/server/graph/schema/schema.graphql index 23cd467c9a..cf20728d62 100644 --- a/server/src/core/server/graph/schema/schema.graphql +++ b/server/src/core/server/graph/schema/schema.graphql @@ -1673,6 +1673,10 @@ EmbeddedCommentsConfiguration specifies the configuration for embedded comments. """ type EmbeddedCommentsConfiguration { allowReplies: Boolean + """ + oEmbedAllowedOrigins are the allowed origins for oEmbed API calls. + """ + oEmbedAllowedOrigins: [String!]! @auth(roles: [ADMIN, MODERATOR]) } """ @@ -1710,7 +1714,6 @@ type BadgeConfiguration { FlairBadgeConfiguration specifies the configuration for flair badges, including whether they are enabled and any configured image urls. """ - type FlairBadge { name: String! url: String! @@ -5568,6 +5571,10 @@ EmbeddedCommentsConfigurationInput specifies the configuration for comment embed """ input EmbeddedCommentsConfigurationInput { allowReplies: Boolean + """ + oEmbedAllowedOrigins are the allowed origins for oEmbed API calls. + """ + oEmbedAllowedOrigins: [String!] } """ diff --git a/server/src/core/server/index.ts b/server/src/core/server/index.ts index 74567c21d7..5a621d9d3e 100644 --- a/server/src/core/server/index.ts +++ b/server/src/core/server/index.ts @@ -382,6 +382,7 @@ class Server { mongo: this.mongo, redis: this.redis, config: this.config, + i18n: this.i18n, mailerQueue: this.tasks.mailer, archiverQueue: this.tasks.archiver, tenantCache: this.tenantCache, diff --git a/server/src/core/server/locales/de-CH/common.ftl b/server/src/core/server/locales/de-CH/common.ftl index c9bc889352..d269633258 100644 --- a/server/src/core/server/locales/de-CH/common.ftl +++ b/server/src/core/server/locales/de-CH/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Respektiert reaction-sortLabelMostRespected = Am meisten Respektiert comment-count = - { $number } { $number -> [one] Kommentar *[other] Kommentare diff --git a/server/src/core/server/locales/de/common.ftl b/server/src/core/server/locales/de/common.ftl index c9bc889352..d269633258 100644 --- a/server/src/core/server/locales/de/common.ftl +++ b/server/src/core/server/locales/de/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Respektiert reaction-sortLabelMostRespected = Am meisten Respektiert comment-count = - { $number } { $number -> [one] Kommentar *[other] Kommentare diff --git a/server/src/core/server/locales/fi-FI/common.ftl b/server/src/core/server/locales/fi-FI/common.ftl index 24dd7041a4..854b5a0332 100644 --- a/server/src/core/server/locales/fi-FI/common.ftl +++ b/server/src/core/server/locales/fi-FI/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Hyvä kommentti reaction-sortLabelMostRespected = Parhaat kommentit comment-count = - { $number } { $number -> [one] kommentti *[other] kommenttia diff --git a/server/src/core/server/locales/fr-FR/common.ftl b/server/src/core/server/locales/fr-FR/common.ftl index bb10b59be2..9a624fc6f8 100755 --- a/server/src/core/server/locales/fr-FR/common.ftl +++ b/server/src/core/server/locales/fr-FR/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Aimé reaction-sortLabelMostRespected = Le plus aimé comment-count = - { $number } { $number -> [one] Commentaire *[other] Commentaires diff --git a/server/src/core/server/locales/pl/common.ftl b/server/src/core/server/locales/pl/common.ftl index a17d9fd260..f15c136f25 100644 --- a/server/src/core/server/locales/pl/common.ftl +++ b/server/src/core/server/locales/pl/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Polecany reaction-sortLabelMostRespected = Najbardziej polecane comment-count = - { $number } { $number -> [one] Komentarz [few] Komentarze diff --git a/server/src/core/server/locales/pt-BR/common.ftl b/server/src/core/server/locales/pt-BR/common.ftl index 0007f3f198..9e9f5dd65e 100644 --- a/server/src/core/server/locales/pt-BR/common.ftl +++ b/server/src/core/server/locales/pt-BR/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Respeitado reaction-sortLabelMostRespected = Mais Respeitados comment-count = - { $number } { $number -> [one] Comentário *[other] Comentários diff --git a/server/src/core/server/locales/sv/common.ftl b/server/src/core/server/locales/sv/common.ftl index 537468ab41..95798c4989 100644 --- a/server/src/core/server/locales/sv/common.ftl +++ b/server/src/core/server/locales/sv/common.ftl @@ -6,7 +6,6 @@ reaction-labelActiveRespected = Respekterad reaction-sortLabelMostRespected = Mest respekterade comment-count = - { $number } { $number -> [one] kommentar *[other] kommentarer diff --git a/server/src/core/server/models/tenant/tenant.ts b/server/src/core/server/models/tenant/tenant.ts index 4e6324523e..cc5343bf4c 100644 --- a/server/src/core/server/models/tenant/tenant.ts +++ b/server/src/core/server/models/tenant/tenant.ts @@ -293,6 +293,7 @@ export async function createTenant( emailDomainModeration: [], embeddedComments: { allowReplies: true, + oEmbedAllowedOrigins: [], }, flairBadges: { flairBadgesEnabled: false, diff --git a/server/src/core/server/queue/tasks/rejector.ts b/server/src/core/server/queue/tasks/rejector.ts index fc1840bb03..995cd8152e 100644 --- a/server/src/core/server/queue/tasks/rejector.ts +++ b/server/src/core/server/queue/tasks/rejector.ts @@ -22,6 +22,7 @@ import { GQLCOMMENT_SORT, GQLCOMMENT_STATUS, } from "coral-server/graph/schema/__generated__/types"; +import { I18n } from "coral-server/services/i18n"; const JOB_NAME = "rejector"; @@ -30,6 +31,7 @@ export interface RejectorProcessorOptions { redis: AugmentedRedis; tenantCache: TenantCache; config: Config; + i18n: I18n; } export interface RejectorData { @@ -75,6 +77,7 @@ const rejectArchivedComments = async ( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, tenant: Readonly, authorID: string, moderatorID: string, @@ -118,6 +121,7 @@ const rejectArchivedComments = async ( mongo, redis, config, + i18n, tenant, input, now, @@ -149,6 +153,7 @@ const rejectLiveComments = async ( redis: AugmentedRedis, cache: DataCache, config: Config, + i18n: I18n, tenant: Readonly, authorID: string, moderatorID: string, @@ -168,6 +173,7 @@ const rejectLiveComments = async ( redis, cache, config, + i18n, null, tenant, comment.id, @@ -197,6 +203,7 @@ const createJobProcessor = redis, tenantCache, config, + i18n, }: RejectorProcessorOptions): JobProcessor => async (job) => { // Pull out the job data. @@ -238,6 +245,7 @@ const createJobProcessor = redis, cache, config, + i18n, tenant, authorID, moderatorID, @@ -248,6 +256,7 @@ const createJobProcessor = mongo, redis, config, + i18n, tenant, authorID, moderatorID, diff --git a/server/src/core/server/services/comments/actions.ts b/server/src/core/server/services/comments/actions.ts index 881baa6126..5083670d93 100644 --- a/server/src/core/server/services/comments/actions.ts +++ b/server/src/core/server/services/comments/actions.ts @@ -40,6 +40,7 @@ import { publishCommentFlagCreated, publishCommentReactionCreated, } from "../events"; +import { I18n } from "../i18n"; import { submitCommentAsSpam } from "../spam"; export type CreateAction = CreateActionInput; @@ -94,6 +95,7 @@ async function addCommentAction( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, broker: CoralEventPublisherBroker, tenant: Tenant, input: Omit, @@ -157,7 +159,7 @@ async function addCommentAction( ); // Update the comment counts onto other documents. - const counts = await updateAllCommentCounts(mongo, redis, config, { + const counts = await updateAllCommentCounts(mongo, redis, config, i18n, { tenant, actionCounts, before: oldComment, @@ -182,6 +184,7 @@ export async function removeCommentAction( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, cache: DataCache, broker: CoralEventPublisherBroker, tenant: Tenant, @@ -250,7 +253,7 @@ export async function removeCommentAction( } // Update the comment counts onto other documents. - const counts = await updateAllCommentCounts(mongo, redis, config, { + const counts = await updateAllCommentCounts(mongo, redis, config, i18n, { tenant, actionCounts, before: oldComment, @@ -280,6 +283,7 @@ export async function createReaction( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, cache: DataCache, broker: CoralEventPublisherBroker, tenant: Tenant, @@ -291,6 +295,7 @@ export async function createReaction( mongo, redis, config, + i18n, broker, tenant, { @@ -331,18 +336,28 @@ export async function removeReaction( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, cache: DataCache, broker: CoralEventPublisherBroker, tenant: Tenant, author: User, input: RemoveCommentReaction ) { - return removeCommentAction(mongo, redis, config, cache, broker, tenant, { - actionType: ACTION_TYPE.REACTION, - commentID: input.commentID, - commentRevisionID: input.commentRevisionID, - userID: author.id, - }); + return removeCommentAction( + mongo, + redis, + config, + i18n, + cache, + broker, + tenant, + { + actionType: ACTION_TYPE.REACTION, + commentID: input.commentID, + commentRevisionID: input.commentRevisionID, + userID: author.id, + } + ); } export type CreateCommentDontAgree = Pick< @@ -354,6 +369,7 @@ export async function createDontAgree( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, commentActionsCache: CommentActionsCache, broker: CoralEventPublisherBroker, tenant: Tenant, @@ -365,6 +381,7 @@ export async function createDontAgree( mongo, redis, config, + i18n, broker, tenant, { @@ -394,18 +411,28 @@ export async function removeDontAgree( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, cache: DataCache, broker: CoralEventPublisherBroker, tenant: Tenant, author: User, input: RemoveCommentDontAgree ) { - return removeCommentAction(mongo, redis, config, cache, broker, tenant, { - actionType: ACTION_TYPE.DONT_AGREE, - commentID: input.commentID, - commentRevisionID: input.commentRevisionID, - userID: author.id, - }); + return removeCommentAction( + mongo, + redis, + config, + i18n, + cache, + broker, + tenant, + { + actionType: ACTION_TYPE.DONT_AGREE, + commentID: input.commentID, + commentRevisionID: input.commentRevisionID, + userID: author.id, + } + ); } export type CreateCommentFlag = Pick< @@ -419,6 +446,7 @@ export async function createFlag( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, commentActionsCache: CommentActionsCache, broker: CoralEventPublisherBroker, tenant: Tenant, @@ -431,6 +459,7 @@ export async function createFlag( mongo, redis, config, + i18n, broker, tenant, { diff --git a/server/src/core/server/services/comments/moderation/moderate.ts b/server/src/core/server/services/comments/moderation/moderate.ts index 2c8cd5a102..8cf597c2df 100644 --- a/server/src/core/server/services/comments/moderation/moderate.ts +++ b/server/src/core/server/services/comments/moderation/moderate.ts @@ -16,6 +16,7 @@ import { updateCommentStatus, } from "coral-server/models/comment"; import { Tenant } from "coral-server/models/tenant"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { updateAllCommentCounts } from "coral-server/stacks/helpers"; @@ -25,6 +26,7 @@ export default async function moderate( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, tenant: Tenant, input: Moderate, now: Date, @@ -113,6 +115,7 @@ export default async function moderate( mongo, redis, config, + i18n, { ...result, tenant, diff --git a/server/src/core/server/services/comments/pipeline/phases/wordList/worker.ts b/server/src/core/server/services/comments/pipeline/phases/wordList/worker.ts index bdbf8445bc..6c6689edf9 100644 --- a/server/src/core/server/services/comments/pipeline/phases/wordList/worker.ts +++ b/server/src/core/server/services/comments/pipeline/phases/wordList/worker.ts @@ -21,6 +21,7 @@ interface List { category: WordListCategory; locale: LanguageCode; regex: RE2 | null; + regexIsEmpty: boolean; } const lists = new Map(); @@ -42,7 +43,13 @@ const initialize = ( const regex = phrases.length > 0 ? createServerWordListRegEx(locale, phrases) : null; - lists.set(key, { tenantID, category, locale, regex }); + lists.set(key, { + tenantID, + category, + locale, + regex, + regexIsEmpty: phrases.length === 0, + }); logger.info( { tenantID, category, phrases: phrases.length }, @@ -73,7 +80,33 @@ const process = ( const listKey = computeWordListKey(tenantID, category); const list = lists.get(listKey); - if (!list || list.regex === null) { + if (!list) { + return { + id, + tenantID, + ok: false, + err: new Error("word list for tenant not found"), + }; + } + + // Handle the case a phrase list is empty. + // If the regex is empty, we had no phrases to match against + // return that there are no matches as there can't be any matches. + if (list.regexIsEmpty) { + return { + id, + tenantID, + ok: true, + data: { + isMatched: false, + matches: [], + }, + }; + } + + // If we made it here, the regex must be valid or something + // has gone very wrong! + if (!list.regex) { return { id, tenantID, diff --git a/server/src/core/server/services/users/delete.ts b/server/src/core/server/services/users/delete.ts index c5a8ff52d0..4e5461bceb 100644 --- a/server/src/core/server/services/users/delete.ts +++ b/server/src/core/server/services/users/delete.ts @@ -10,6 +10,7 @@ import { retrieveTenant } from "coral-server/models/tenant"; import { GQLCOMMENT_STATUS } from "coral-server/graph/schema/__generated__/types"; import { moderate } from "../comments/moderation"; +import { I18n } from "../i18n"; import { AugmentedRedis } from "../redis"; const BATCH_SIZE = 500; @@ -117,6 +118,7 @@ async function moderateComments( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, tenantID: string, filter: FilterQuery, targetStatus: GQLCOMMENT_STATUS, @@ -152,6 +154,7 @@ async function moderateComments( mongo, redis, config, + i18n, tenant, { commentID: comment.id, @@ -174,6 +177,7 @@ async function deleteUserComments( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, authorID: string, tenantID: string, now: Date, @@ -186,6 +190,7 @@ async function deleteUserComments( mongo, redis, config, + i18n, tenantID, { tenantID, @@ -204,6 +209,7 @@ async function deleteUserComments( mongo, redis, config, + i18n, tenantID, { tenantID, @@ -243,6 +249,7 @@ export async function deleteUser( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, userID: string, tenantID: string, now: Date @@ -269,9 +276,18 @@ export async function deleteUser( } // Delete the user's comments. - await deleteUserComments(mongo, redis, config, userID, tenantID, now); + await deleteUserComments(mongo, redis, config, i18n, userID, tenantID, now); if (mongo.archive) { - await deleteUserComments(mongo, redis, config, userID, tenantID, now, true); + await deleteUserComments( + mongo, + redis, + config, + i18n, + userID, + tenantID, + now, + true + ); } // Mark the user as deleted. diff --git a/server/src/core/server/stacks/approveComment.ts b/server/src/core/server/stacks/approveComment.ts index 58a50394f6..e98c894c82 100644 --- a/server/src/core/server/stacks/approveComment.ts +++ b/server/src/core/server/stacks/approveComment.ts @@ -5,6 +5,7 @@ import { CoralEventPublisherBroker } from "coral-server/events/publisher"; import { getLatestRevision } from "coral-server/models/comment"; import { Tenant } from "coral-server/models/tenant"; import { moderate } from "coral-server/services/comments/moderation"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { submitCommentAsNotSpam } from "coral-server/services/spam"; import { Request } from "coral-server/types/express"; @@ -18,6 +19,7 @@ const approveComment = async ( redis: AugmentedRedis, cache: DataCache, config: Config, + i18n: I18n, broker: CoralEventPublisherBroker, tenant: Tenant, commentID: string, @@ -33,6 +35,7 @@ const approveComment = async ( mongo, redis, config, + i18n, tenant, { commentID, diff --git a/server/src/core/server/stacks/createComment.ts b/server/src/core/server/stacks/createComment.ts index 7fc240dd27..65aab158da 100644 --- a/server/src/core/server/stacks/createComment.ts +++ b/server/src/core/server/stacks/createComment.ts @@ -57,6 +57,7 @@ import { processForModeration, } from "coral-server/services/comments/pipeline"; import { WordListService } from "coral-server/services/comments/pipeline/phases/wordList/service"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { updateUserLastCommentID } from "coral-server/services/users"; import { Request } from "coral-server/types/express"; @@ -95,6 +96,7 @@ const markCommentAsAnswered = async ( redis: AugmentedRedis, cache: DataCache, config: Config, + i18n: I18n, broker: CoralEventPublisherBroker, tenant: Tenant, comment: Readonly, @@ -144,6 +146,7 @@ const markCommentAsAnswered = async ( redis, cache, config, + i18n, broker, tenant, comment.parentID, @@ -201,6 +204,7 @@ export default async function create( wordList: WordListService, cache: DataCache, config: Config, + i18n: I18n, broker: CoralEventPublisherBroker, tenant: Tenant, author: User, @@ -386,6 +390,7 @@ export default async function create( redis, cache, config, + i18n, broker, tenant, comment, @@ -443,7 +448,7 @@ export default async function create( } // Update all the comment counts on stories and users. - const counts = await updateAllCommentCounts(mongo, redis, config, { + const counts = await updateAllCommentCounts(mongo, redis, config, i18n, { tenant, actionCounts, after: comment, diff --git a/server/src/core/server/stacks/editComment.ts b/server/src/core/server/stacks/editComment.ts index 78e4f559b8..0981d178c4 100644 --- a/server/src/core/server/stacks/editComment.ts +++ b/server/src/core/server/stacks/editComment.ts @@ -44,6 +44,7 @@ import { } from "coral-server/services/comments/media"; import { processForModeration } from "coral-server/services/comments/pipeline"; import { WordListService } from "coral-server/services/comments/pipeline/phases/wordList/service"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { Request } from "coral-server/types/express"; @@ -83,6 +84,7 @@ export default async function edit( wordList: WordListService, cache: DataCache, config: Config, + i18n: I18n, broker: CoralEventPublisherBroker, tenant: Tenant, author: User, @@ -103,7 +105,7 @@ export default async function edit( throw new CommentNotFoundError(input.id); } - // If the original comment was a reply, then get it's parent! + // If the original comment was a reply, then get its parent! const { parentID, parentRevisionID, siteID } = originalStaleComment; const parent = await retrieveParent(mongo, tenant.id, { parentID, @@ -271,7 +273,7 @@ export default async function edit( } // Update all the comment counts on stories and users. - const counts = await updateAllCommentCounts(mongo, redis, config, { + const counts = await updateAllCommentCounts(mongo, redis, config, i18n, { tenant, actionCounts, ...result, diff --git a/server/src/core/server/stacks/helpers/retrieveParent.ts b/server/src/core/server/stacks/helpers/retrieveParent.ts index 19798d1900..b55d641053 100644 --- a/server/src/core/server/stacks/helpers/retrieveParent.ts +++ b/server/src/core/server/stacks/helpers/retrieveParent.ts @@ -5,7 +5,6 @@ import { ParentCommentRejectedError, } from "coral-server/errors"; import { - getLatestRevision, hasPublishedStatus, retrieveComment, } from "coral-server/models/comment"; @@ -31,13 +30,6 @@ async function retrieveParent( throw new CommentNotFoundError(input.parentID); } - // Check to see that the most recent revision matches the one we just replied - // to. - const revision = getLatestRevision(parent); - if (revision.id !== input.parentRevisionID) { - throw new CommentRevisionNotFoundError(parent.id, input.parentRevisionID); - } - // Check that the parent comment was visible. if (!hasPublishedStatus(parent)) { if (parent.status === GQLCOMMENT_STATUS.REJECTED) { diff --git a/server/src/core/server/stacks/helpers/updateAllCommentCounts.ts b/server/src/core/server/stacks/helpers/updateAllCommentCounts.ts index 98263e80d6..fca81158da 100644 --- a/server/src/core/server/stacks/helpers/updateAllCommentCounts.ts +++ b/server/src/core/server/stacks/helpers/updateAllCommentCounts.ts @@ -1,3 +1,4 @@ +import { getTextHTML } from "coral-server/app/handlers"; import { get, getCountRedisCacheKey } from "coral-server/app/middleware/cache"; import { Config } from "coral-server/config"; import { MongoContext } from "coral-server/data/context"; @@ -19,6 +20,7 @@ import { calculateCounts, calculateCountsDiff, } from "coral-server/services/comments/moderation"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { @@ -139,6 +141,7 @@ export default async function updateAllCommentCounts( mongo: MongoContext, redis: AugmentedRedis, config: Config, + i18n: I18n, input: UpdateAllCommentCountsInput, options: UpdateAllCommentCountsOptions = { updateStory: true, @@ -186,12 +189,21 @@ export default async function updateAllCommentCounts( if (entry) { const { body } = entry; - // update count in jsonp data with new total comment count + // update count and textHtml in jsonp data with new total comment count + // and matching localized textHtml const bodyArr = body.split(","); for (let i = 0; i < bodyArr.length; i++) { if (bodyArr[i].startsWith('"count":')) { bodyArr[i] = `"count":${totalCount}`; - break; + } + if (bodyArr[i].startsWith('"textHtml":')) { + const textHtml = getTextHTML( + tenant, + updatedStory.settings.mode, + i18n, + totalCount + ); + bodyArr[i] = `"textHtml":"${textHtml.replace(/"/g, '\\"')}"`; } } const updatedEntry = { diff --git a/server/src/core/server/stacks/rejectComment.ts b/server/src/core/server/stacks/rejectComment.ts index 3b98ba01e8..701949cab5 100644 --- a/server/src/core/server/stacks/rejectComment.ts +++ b/server/src/core/server/stacks/rejectComment.ts @@ -11,6 +11,7 @@ import { import { Tenant } from "coral-server/models/tenant"; import { removeTag } from "coral-server/services/comments"; import { moderate } from "coral-server/services/comments/moderation"; +import { I18n } from "coral-server/services/i18n"; import { AugmentedRedis } from "coral-server/services/redis"; import { submitCommentAsSpam } from "coral-server/services/spam"; import { Request } from "coral-server/types/express"; @@ -68,6 +69,7 @@ const rejectComment = async ( redis: AugmentedRedis, cache: DataCache, config: Config, + i18n: I18n, broker: CoralEventPublisherBroker | null, tenant: Tenant, commentID: string, @@ -85,6 +87,7 @@ const rejectComment = async ( mongo, redis, config, + i18n, tenant, { commentID, diff --git a/server/src/core/server/test/fixtures.ts b/server/src/core/server/test/fixtures.ts index 65ce9cea59..0abca1f9e2 100644 --- a/server/src/core/server/test/fixtures.ts +++ b/server/src/core/server/test/fixtures.ts @@ -184,6 +184,10 @@ export const createTenantFixture = ( flattenReplies: false, disableDefaultFonts: false, emailDomainModeration: [], + embeddedComments: { + allowReplies: true, + oEmbedAllowedOrigins: [], + }, }; return merge(fixture, defaults);