diff --git a/LICENSE b/LICENSE index 50eddcc871..954e04f61b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2020 Vox Media, Inc +Copyright 2024 Vox Media, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/package-lock.json b/client/package-lock.json index b6b2330e49..e4d6519169 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "@coralproject/talk", - "version": "8.7.1", + "version": "8.7.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@coralproject/talk", - "version": "8.7.1", + "version": "8.7.2", "license": "Apache-2.0", "dependencies": { "@ampproject/toolbox-cache-url": "^2.9.0", diff --git a/client/package.json b/client/package.json index 513e14809a..054249f74b 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@coralproject/talk", - "version": "8.7.1", + "version": "8.7.2", "author": "The Coral Project", "homepage": "https://coralproject.net/", "sideEffects": [ diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/AccountHistoryAction.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/AccountHistoryAction.tsx index ddcaa6307f..43ffba5078 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/AccountHistoryAction.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/AccountHistoryAction.tsx @@ -5,6 +5,9 @@ import ModMessageAction, { ModMessageActionProps } from "./ModMessageAction"; import PremodAction, { PremodActionProps } from "./PremodAction"; import SiteBanAction from "./SiteBanAction"; import SuspensionAction, { SuspensionActionProps } from "./SuspensionAction"; +import UserDeletionAction, { + UserDeletionActionProps, +} from "./UserDeletionAction"; import UsernameChangeAction, { UsernameChangeActionProps, } from "./UsernameChangeAction"; @@ -18,14 +21,16 @@ export interface HistoryActionProps { | "site-ban" | "premod" | "warning" - | "modMessage"; + | "modMessage" + | "deletion"; action: | UsernameChangeActionProps | SuspensionActionProps | BanActionProps | PremodActionProps | WarningActionProps - | ModMessageActionProps; + | ModMessageActionProps + | UserDeletionActionProps; } const AccountHistoryAction: FunctionComponent = ({ @@ -49,6 +54,8 @@ const AccountHistoryAction: FunctionComponent = ({ return ; case "modMessage": return ; + case "deletion": + return ; default: return null; } diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/CancelScheduledAccountDeletionMutation.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/CancelScheduledAccountDeletionMutation.tsx new file mode 100644 index 0000000000..17ea643321 --- /dev/null +++ b/client/src/core/client/admin/components/UserHistoryDrawer/CancelScheduledAccountDeletionMutation.tsx @@ -0,0 +1,55 @@ +import { graphql } from "react-relay"; +import { Environment } from "relay-runtime"; + +import { + commitMutationPromiseNormalized, + createMutation, + MutationInput, +} from "coral-framework/lib/relay"; + +import { CancelScheduledAccountDeletionMutation as MutationTypes } from "coral-admin/__generated__/CancelScheduledAccountDeletionMutation.graphql"; + +let clientMutationId = 0; + +const CancelScheduledAccountDeletionMutation = createMutation( + "cancelScheduleAccountDeletion", + async (environment: Environment, input: MutationInput) => { + const result = await commitMutationPromiseNormalized( + environment, + { + mutation: graphql` + mutation CancelScheduledAccountDeletionMutation( + $input: CancelScheduledAccountDeletionInput! + ) { + cancelScheduledAccountDeletion(input: $input) { + user { + scheduledDeletionDate + status { + deletion { + history { + updateType + createdBy { + username + } + createdAt + } + } + } + } + clientMutationId + } + } + `, + variables: { + input: { + userID: input.userID, + clientMutationId: (clientMutationId++).toString(), + }, + }, + } + ); + return result; + } +); + +export default CancelScheduledAccountDeletionMutation; diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.css b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.css new file mode 100644 index 0000000000..071451298e --- /dev/null +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.css @@ -0,0 +1,95 @@ +.root { + width: 280px; + font-family: var(--font-family-primary); + max-width: 80vw; + text-align: left; +} + +.title { + font-family: var(--font-family-primary); + font-weight: var(--font-weight-primary-semi-bold); + font-size: var(--font-size-3); + line-height: 1; + + color: var(--palette-text-500); +} + +.header { + font-family: var(--font-family-primary); + font-weight: var(--font-weight-primary-bold); + font-size: var(--font-size-2); + color: var(--palette-text-500); + margin-bottom: var(--spacing-1); + margin-top: var(--spacing-3); +} + +.username { + font-size: var(--font-size-2); +} + +.orderedList { + margin: 0; + padding-left: var(--spacing-4); + + li { + font-family: var(--font-family-primary); + font-size: var(--font-size-2); + } +} + +.callOut { + padding: var(--spacing-1); + font-weight: var(--font-weight-primary-semi-bold); + margin-top: var(--spacing-3); + font-size: var(--font-size-1); +} + +.moreInfo { + font-size: var(--font-size-2); + margin-top: var(--spacing-2); +} + +.icon { + display: inline-block; + margin-right: var(--spacing-1); + position: relative; + top: 2px; +} + +.confirmationInput { + box-sizing: border-box; + font-family: var(--font-family-primary); + font-size: var(--font-size-2); + line-height: 2.25; + padding-left: var(--spacing-2); + width: 100%; +} + +.description { + font-family: var(--font-family-primary); + font-weight: var(--font-weight-primary-regular); + font-size: var(--font-size-3); + line-height: 1; + + color: var(--palette-text-500); +} + +.actions { + padding-top: var(--spacing-3); +} + +.link { + display: inline; + vertical-align: baseline; + white-space: break-spaces; +} + +.container { + margin-top: var(--spacing-2); +} + +.error { + color: var(--palette-error-500); + margin-top: var(--spacing-2); + font-weight: var(--font-weight-primary-semi-bold); +} diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx new file mode 100644 index 0000000000..06eab24eb2 --- /dev/null +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx @@ -0,0 +1,160 @@ +import { Localized } from "@fluent/react/compat"; +import React, { + FunctionComponent, + useCallback, + useMemo, + useState, +} from "react"; + +import { useMutation } from "coral-framework/lib/relay"; +import { + AlertCircleIcon, + AlertTriangleIcon, + SvgIcon, +} from "coral-ui/components/icons"; +import { Box, Button, CallOut, Flex } from "coral-ui/components/v2"; + +import ScheduleAccountDeletionMutation from "./ScheduleAccountDeletionMutation"; + +import styles from "./DeleteAccountPopover.css"; + +interface Props { + onDismiss: () => void; + userID: string; + username: string | null; +} + +const DeleteAccountPopover: FunctionComponent = ({ + onDismiss, + userID, + username, +}) => { + const scheduleAccountDeletion = useMutation(ScheduleAccountDeletionMutation); + const [requestDeletionError, setRequestDeletionError] = useState< + string | null + >(null); + + const onRequestDeletion = useCallback(async () => { + try { + await scheduleAccountDeletion({ userID }); + } catch (e) { + if (e.message) { + setRequestDeletionError(e.message as string); + } + } + }, [userID, scheduleAccountDeletion, setRequestDeletionError]); + + const deleteAccountConfirmationText = "delete"; + const [ + deleteAccountConfirmationTextInput, + setDeleteAccountConfirmationTextInput, + ] = useState(""); + + const onDeleteAccountConfirmationTextInputChange = useCallback( + (e: React.ChangeEvent) => { + setDeleteAccountConfirmationTextInput(e.target.value); + }, + [setDeleteAccountConfirmationTextInput] + ); + + const deleteAccountButtonDisabled = useMemo(() => { + return !( + deleteAccountConfirmationTextInput.toLowerCase() === + deleteAccountConfirmationText + ); + }, [deleteAccountConfirmationText, deleteAccountConfirmationTextInput]); + + return ( + + <> + +
Delete account
+
+ +
Username
+
+
{username ?? ""}
+ +
Delete account will
+
+
+
    + +
  1. + Remove all comments written by this user from the database. +
  2. +
    + +
  3. + Delete all record of this account. The user could then create a + new account using the same email address. If you want to Ban + this user instead and retain their history, press "CANCEL" and + use the Status dropdown below the username. +
  4. +
    +
+
+ + + + This removes all records of this user + + + +
+ This will go into effect in 24 hours. +
+
+ +
+ Type in "{deleteAccountConfirmationText}" to confirm +
+
+ + {requestDeletionError && ( +
+ + {requestDeletionError} +
+ )} + + + + + + + + + +
+ ); +}; + +export default DeleteAccountPopover; diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css new file mode 100644 index 0000000000..783cb33ad2 --- /dev/null +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css @@ -0,0 +1,26 @@ +.icon { + display: inline-block; + margin-right: var(--spacing-1); + position: relative; + top: 2px; +} + +.error { + color: var(--palette-error-500); + margin-top: var(--spacing-2); + font-weight: var(--font-weight-primary-semi-bold); +} + +.deletionCalloutTitle { + font-weight: var(--font-weight-primary-semi-bold); + font-size: var(--font-size-3); +} + +.deletionCalloutInfo { + margin-top: var(--spacing-2); + margin-bottom: var(--spacing-1); +} + +.cancelDeletionButton { + margin-top: var(--spacing-2); +} diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx new file mode 100644 index 0000000000..df50e585d1 --- /dev/null +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx @@ -0,0 +1,139 @@ +import { Localized } from "@fluent/react/compat"; +import React, { + FunctionComponent, + useCallback, + useMemo, + useState, +} from "react"; +import { graphql } from "react-relay"; + +import { useDateTimeFormatter } from "coral-framework/hooks"; +import { useMutation, withFragmentContainer } from "coral-framework/lib/relay"; +import { AlertCircleIcon, SvgIcon } from "coral-ui/components/icons"; +import { Button, CallOut, ClickOutside, Popover } from "coral-ui/components/v2"; + +import { DeleteAccountPopoverContainer_user as UserData } from "coral-admin/__generated__/DeleteAccountPopoverContainer_user.graphql"; + +import CancelScheduledAccountDeletionMutation from "./CancelScheduledAccountDeletionMutation"; +import DeleteAccountPopover from "./DeleteAccountPopover"; + +import styles from "./DeleteAccountPopoverContainer.css"; + +interface Props { + user: UserData; +} + +const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { + const cancelScheduledAccountDeletion = useMutation( + CancelScheduledAccountDeletionMutation + ); + const [cancelDeletionError, setCancelDeletionError] = useState( + null + ); + + const formatter = useDateTimeFormatter({ + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }); + + const onCancelScheduledDeletion = useCallback(async () => { + try { + await cancelScheduledAccountDeletion({ userID: user.id }); + } catch (e) { + if (e.message) { + setCancelDeletionError(e.message as string); + } + } + }, [user.id, cancelScheduledAccountDeletion, setCancelDeletionError]); + + const deletionDate = useMemo( + () => + user.scheduledDeletionDate ? formatter(user.scheduledDeletionDate) : null, + [user, formatter] + ); + + if (deletionDate) { + return ( + + +
+ User deletion activated +
+
+ +
+ This will occur at {deletionDate}. +
+
+ + + + {cancelDeletionError && ( +
+ {" "} + + {cancelDeletionError} +
+ )} +
+ ); + } + + return ( + + ( + + + + )} + > + {({ toggleVisibility, ref }) => ( + + + + )} + + + ); +}; + +const enhanced = withFragmentContainer({ + user: graphql` + fragment DeleteAccountPopoverContainer_user on User { + id + username + scheduledDeletionDate + } + `, +})(DeleteAccountPopoverContainer); + +export default enhanced; diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/ScheduleAccountDeletionMutation.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/ScheduleAccountDeletionMutation.tsx new file mode 100644 index 0000000000..3979dd1428 --- /dev/null +++ b/client/src/core/client/admin/components/UserHistoryDrawer/ScheduleAccountDeletionMutation.tsx @@ -0,0 +1,55 @@ +import { graphql } from "react-relay"; +import { Environment } from "relay-runtime"; + +import { + commitMutationPromiseNormalized, + createMutation, + MutationInput, +} from "coral-framework/lib/relay"; + +import { ScheduleAccountDeletionMutation as MutationTypes } from "coral-admin/__generated__/ScheduleAccountDeletionMutation.graphql"; + +let clientMutationId = 0; + +const ScheduleAccountDeletionMutation = createMutation( + "scheduleAccountDeletion", + async (environment: Environment, input: MutationInput) => { + const result = await commitMutationPromiseNormalized( + environment, + { + mutation: graphql` + mutation ScheduleAccountDeletionMutation( + $input: ScheduleAccountDeletionInput! + ) { + scheduleAccountDeletion(input: $input) { + user { + scheduledDeletionDate + status { + deletion { + history { + updateType + createdBy { + username + } + createdAt + } + } + } + } + clientMutationId + } + } + `, + variables: { + input: { + userID: input.userID, + clientMutationId: (clientMutationId++).toString(), + }, + }, + } + ); + return result; + } +); + +export default ScheduleAccountDeletionMutation; diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/UserDeletionAction.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/UserDeletionAction.tsx new file mode 100644 index 0000000000..976c0c8bc5 --- /dev/null +++ b/client/src/core/client/admin/components/UserHistoryDrawer/UserDeletionAction.tsx @@ -0,0 +1,26 @@ +import { Localized } from "@fluent/react/compat"; +import React, { FunctionComponent } from "react"; + +export interface UserDeletionActionProps { + action: "CANCELED" | "REQUESTED" | "%future added value"; +} + +const UserDeletionAction: FunctionComponent = ({ + action, +}) => { + if (action === "REQUESTED") { + return ( + + User scheduled for deletion + + ); + } else if (action === "CANCELED") { + return ( + + User deletion request canceled + + ); + } + return null; +}; +export default UserDeletionAction; diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.css b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.css index a8abf8974f..567aeb9202 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.css +++ b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.css @@ -59,3 +59,7 @@ padding-right: var(--spacing-1); vertical-align: middle; } + +.deleteButtonWrapper { + margin-bottom: var(--spacing-2); +} diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx index 4446d57386..2fd93557c4 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx @@ -1,9 +1,13 @@ import { Localized } from "@fluent/react/compat"; -import React, { FunctionComponent, useMemo } from "react"; +import React, { FunctionComponent, useCallback, useMemo } from "react"; import { graphql } from "react-relay"; +import { SCHEDULED_DELETION_WINDOW_DURATION } from "coral-common/common/lib/constants"; import { useDateTimeFormatter } from "coral-framework/hooks"; +import { useCoralContext } from "coral-framework/lib/bootstrap"; +import { getMessage } from "coral-framework/lib/i18n"; import { withFragmentContainer } from "coral-framework/lib/relay"; +import { GQLUSER_ROLE } from "coral-framework/schema"; import { CoralMarkIcon, SvgIcon } from "coral-ui/components/icons"; import { CallOut, @@ -16,16 +20,19 @@ import { } from "coral-ui/components/v2"; import { UserDrawerAccountHistory_user } from "coral-admin/__generated__/UserDrawerAccountHistory_user.graphql"; +import { UserDrawerAccountHistory_viewer } from "coral-admin/__generated__/UserDrawerAccountHistory_viewer.graphql"; import AccountHistoryAction, { HistoryActionProps, } from "./AccountHistoryAction"; import { BanActionProps } from "./BanAction"; +import DeleteAccountPopoverContainer from "./DeleteAccountPopoverContainer"; import styles from "./UserDrawerAccountHistory.css"; interface Props { user: UserDrawerAccountHistory_user; + viewer: UserDrawerAccountHistory_viewer; } interface From { @@ -39,7 +46,10 @@ type HistoryRecord = HistoryActionProps & { description?: string | null; }; -const UserDrawerAccountHistory: FunctionComponent = ({ user }) => { +const UserDrawerAccountHistory: FunctionComponent = ({ + user, + viewer, +}) => { const system = ( = ({ user }) => { ); + + const { localeBundles } = useCoralContext(); + + const deletionFormatter = useDateTimeFormatter({ + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }); + const addSeconds = (date: Date, seconds: number) => { + return new Date(date.getTime() + seconds * 1000); + }; + + const deletionDescriptionMapping = useCallback( + (updateType: string, createdAt: string) => { + const mapping: { [key: string]: string } = { + REQUESTED: getMessage( + localeBundles, + "moderate-user-drawer-account-history-deletion-scheduled", + `Deletion scheduled for ${deletionFormatter( + addSeconds(new Date(createdAt), SCHEDULED_DELETION_WINDOW_DURATION) + )}`, + { + createdAt: deletionFormatter( + addSeconds( + new Date(createdAt), + SCHEDULED_DELETION_WINDOW_DURATION + ) + ), + } + ), + CANCELED: getMessage( + localeBundles, + "moderate-user-drawer-account-history-canceled-at", + `Canceled at ${deletionFormatter(new Date(createdAt))}`, + { createdAt: deletionFormatter(new Date(createdAt)) } + ), + "%future added value": getMessage( + localeBundles, + "moderate-user-drawer-account-history-updated-at", + `Updated at ${deletionFormatter(new Date(createdAt))}`, + { createdAt: deletionFormatter(new Date(createdAt)) } + ), + }; + return mapping[updateType]; + }, + [getMessage, localeBundles, addSeconds, deletionFormatter] + ); + const combinedHistory = useMemo(() => { // Collect all the different types of history items. const history: HistoryRecord[] = []; @@ -217,6 +278,19 @@ const UserDrawerAccountHistory: FunctionComponent = ({ user }) => { }); }); + user.status.deletion.history.forEach((record) => { + history.push({ + kind: "deletion", + date: new Date(record.createdAt), + takenBy: record.createdBy ? record.createdBy.username : system, + action: { action: record.updateType }, + description: deletionDescriptionMapping( + record.updateType, + record.createdAt + ), + }); + }); + // Sort the history so that it's in the right order. const dateSortedHistory = history.sort( (a, b) => b.date.getTime() - a.date.getTime() @@ -227,7 +301,7 @@ const UserDrawerAccountHistory: FunctionComponent = ({ user }) => { } return dateSortedHistory; - }, [system, user.status]); + }, [system, user.status, deletionDescriptionMapping]); const formatter = useDateTimeFormatter({ year: "numeric", month: "long", @@ -236,11 +310,18 @@ const UserDrawerAccountHistory: FunctionComponent = ({ user }) => { if (combinedHistory.length === 0) { return ( - - - No actions have been taken on this account - - + <> + {viewer.role === GQLUSER_ROLE.ADMIN && ( +
+ +
+ )} + + + No actions have been taken on this account + + + ); } @@ -263,6 +344,9 @@ const UserDrawerAccountHistory: FunctionComponent = ({ user }) => { return ( + {viewer.role === GQLUSER_ROLE.ADMIN && ( + + )} @@ -294,8 +378,15 @@ const UserDrawerAccountHistory: FunctionComponent = ({ user }) => { }; const enhanced = withFragmentContainer({ + viewer: graphql` + fragment UserDrawerAccountHistory_viewer on User { + id + role + } + `, user: graphql` fragment UserDrawerAccountHistory_user on User { + ...DeleteAccountPopoverContainer_user status { username { history { @@ -306,6 +397,15 @@ const enhanced = withFragmentContainer({ } } } + deletion { + history { + updateType + createdAt + createdBy { + username + } + } + } warning { history { active diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistoryQuery.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistoryQuery.tsx index a980d7669e..38bbf68b8f 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistoryQuery.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistoryQuery.tsx @@ -26,6 +26,9 @@ const UserDrawerAccountHistoryQuery: FunctionComponent = ({ user(id: $userID) { ...UserDrawerAccountHistory_user } + viewer { + ...UserDrawerAccountHistory_viewer + } } `} variables={{ userID }} @@ -55,7 +58,9 @@ const UserDrawerAccountHistoryQuery: FunctionComponent = ({ ); } - return ; + return ( + + ); }} /> ); diff --git a/client/src/core/client/admin/routes/Configure/sections/General/MediaLinksConfig.tsx b/client/src/core/client/admin/routes/Configure/sections/General/MediaLinksConfig.tsx index acc9dd4d3b..28d9adcaf7 100644 --- a/client/src/core/client/admin/routes/Configure/sections/General/MediaLinksConfig.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/General/MediaLinksConfig.tsx @@ -64,13 +64,13 @@ const MediaLinksConfig: FunctionComponent = ({ disabled }) => { > - Allow commenters to add a YouTube video, Tweet or GIF from GIPHY's + Allow commenters to add a YouTube video, X post or GIF from GIPHY's library to the end of their comment - + = ({ disabled }) => { - + ).toBeInTheDocument(); }); +it("user drawer is open and user can be scheduled for deletion and have deletion canceled", async () => { + const user = users.commenters[0]; + const resolvers = createResolversStub({ + Mutation: { + scheduleAccountDeletion: ({ variables }) => { + expectAndFail(variables).toMatchObject({ + userID: user.id, + }); + const userRecord = pureMerge(user, { + scheduledDeletionDate: "2024-01-11T20:48:20.317+00:00", + status: { + deletion: { + history: [ + { + updateType: GQLUserDeletionUpdateType.REQUESTED, + createdAt: "2018-11-29T16:01:51.897Z", + createdBy: viewer, + }, + ], + }, + }, + }); + return { + user: userRecord, + }; + }, + cancelScheduledAccountDeletion: ({ variables }) => { + expectAndFail(variables).toMatchObject({ + userID: user.id, + }); + const userRecord = pureMerge(user, { + scheduledDeletionDate: null, + status: { + deletion: { + history: [ + { + updateType: GQLUserDeletionUpdateType.REQUESTED, + createdAt: "2024-01-10T16:01:51.897Z", + createdBy: viewer, + }, + { + updateType: GQLUserDeletionUpdateType.CANCELED, + createdAt: "2024-01-10T16:25:51.897Z", + createdBy: viewer, + }, + ], + }, + }, + }); + return { + user: userRecord, + }; + }, + }, + }); + await createTestRenderer({ resolvers }); + await screen.findByTestId("community-container"); + const isabelle = await screen.findByRole("button", { name: "Isabelle" }); + await act(async () => { + userEvent.click(isabelle); + }); + const isabelleUserHistory = await screen.findByTestId( + "userHistoryDrawer-modal" + ); + const historyTab = await within(isabelleUserHistory).findByRole("tab", { + name: "Tab: time-reverse Account History", + }); + await act(async () => { + userEvent.click(historyTab); + }); + const tabRegion = screen.getByRole("region", { + name: "Tab: time-reverse Account History", + }); + const deleteAccountButton = within(tabRegion).getByRole("button", { + name: "Delete account", + }); + expect(deleteAccountButton).toBeVisible(); + + await act(async () => { + userEvent.click(deleteAccountButton); + }); + const popover = screen.getByRole("dialog", { + name: "A popover menu to delete a user's account", + }); + const deleteButton = within(popover).getByRole("button", { name: "Delete" }); + expect(deleteButton).toBeDisabled(); + const input = within(popover).getByRole("textbox"); + fireEvent.change(input, { target: { value: "delete" } }); + + expect(deleteButton).toBeEnabled(); + fireEvent.click(deleteButton); + expect(resolvers.Mutation!.scheduleAccountDeletion!.called).toBe(true); + await screen.findByText("User deletion activated"); + const cancelDeletionButton = screen.getByRole("button", { + name: "Cancel user deletion", + }); + userEvent.click(cancelDeletionButton); + expect(resolvers.Mutation!.cancelScheduledAccountDeletion!.called).toBe(true); +}); + it("opens user drawer and shows external profile url link when has feature flag and configured", async () => { const settingsOverride = settings; settingsOverride.featureFlags = [ diff --git a/client/src/core/client/admin/test/fixtures.ts b/client/src/core/client/admin/test/fixtures.ts index 0d06200721..1fb1a1db2d 100644 --- a/client/src/core/client/admin/test/fixtures.ts +++ b/client/src/core/client/admin/test/fixtures.ts @@ -442,6 +442,12 @@ export const baseUser = createFixture({ active: false, history: [], }, + deletion: { + history: [], + }, + username: { + history: [], + }, }, }); diff --git a/client/src/core/client/stream/tabs/Comments/Comment/MediaConfirmation/MediaConfirmPrompt.tsx b/client/src/core/client/stream/tabs/Comments/Comment/MediaConfirmation/MediaConfirmPrompt.tsx index c0a2aea897..e05c1df6a7 100644 --- a/client/src/core/client/stream/tabs/Comments/Comment/MediaConfirmation/MediaConfirmPrompt.tsx +++ b/client/src/core/client/stream/tabs/Comments/Comment/MediaConfirmation/MediaConfirmPrompt.tsx @@ -47,7 +47,7 @@ const MediaConfirmPrompt: FunctionComponent = ({ {media.type === "twitter" && (

- Add this tweet to the end of your comment? + Add this post to the end of your comment?

)} @@ -72,7 +72,7 @@ const MediaConfirmPrompt: FunctionComponent = ({ onClick={onConfirm} className={styles.promptButton} > - Add tweet + Add post )} diff --git a/client/src/core/client/stream/tabs/Comments/Comment/MediaConfirmation/MediaConfirmationIcon.css b/client/src/core/client/stream/tabs/Comments/Comment/MediaConfirmation/MediaConfirmationIcon.css deleted file mode 100644 index 4a68c55d27..0000000000 --- a/client/src/core/client/stream/tabs/Comments/Comment/MediaConfirmation/MediaConfirmationIcon.css +++ /dev/null @@ -1,4 +0,0 @@ -.twitterIcon svg { - color: #00acee; - fill: #00acee; -} diff --git a/client/src/core/client/stream/tabs/Comments/Comment/MediaConfirmation/MediaConfirmationIcon.tsx b/client/src/core/client/stream/tabs/Comments/Comment/MediaConfirmation/MediaConfirmationIcon.tsx index adb42a11a1..b2769f66e6 100644 --- a/client/src/core/client/stream/tabs/Comments/Comment/MediaConfirmation/MediaConfirmationIcon.tsx +++ b/client/src/core/client/stream/tabs/Comments/Comment/MediaConfirmation/MediaConfirmationIcon.tsx @@ -3,11 +3,10 @@ import React, { FunctionComponent } from "react"; import { MediaLink } from "coral-common/common/lib/helpers/findMediaLinks"; import { ImageFileLandscapeIcon, - SocialMediaTwitterIcon, SvgIcon, VideoPlayerIcon, + XLogoTwitterIcon, } from "coral-ui/components/icons"; -import styles from "./MediaConfirmationIcon.css"; interface Props { media: MediaLink; @@ -19,11 +18,7 @@ const MediaConfirmationIcon: FunctionComponent = ({ media }) => { {media.type === "external" && } {media.type === "youtube" && } {media.type === "twitter" && ( - + )} ); diff --git a/client/src/core/client/stream/tabs/Comments/Comment/MediaSection/MediaSectionContainer.tsx b/client/src/core/client/stream/tabs/Comments/Comment/MediaSection/MediaSectionContainer.tsx index 518e1a93a3..37ca91c655 100644 --- a/client/src/core/client/stream/tabs/Comments/Comment/MediaSection/MediaSectionContainer.tsx +++ b/client/src/core/client/stream/tabs/Comments/Comment/MediaSection/MediaSectionContainer.tsx @@ -100,9 +100,7 @@ const MediaSectionContainer: FunctionComponent = ({ > {media.__typename === "TwitterMedia" && ( - - Show Tweet - + Show post )} {media.__typename === "YouTubeMedia" && ( @@ -139,7 +137,7 @@ const MediaSectionContainer: FunctionComponent = ({ /> {media.__typename === "TwitterMedia" && ( - Hide Tweet + Hide post )} {media.__typename === "GiphyMedia" && ( diff --git a/client/src/core/client/stream/tabs/Comments/Comment/ReplyEditSubmitStatus.tsx b/client/src/core/client/stream/tabs/Comments/Comment/ReplyEditSubmitStatus.tsx index ff9a58b857..c7315509ed 100644 --- a/client/src/core/client/stream/tabs/Comments/Comment/ReplyEditSubmitStatus.tsx +++ b/client/src/core/client/stream/tabs/Comments/Comment/ReplyEditSubmitStatus.tsx @@ -33,7 +33,8 @@ function getMessage( title={ - This comment has been rejected for violating our guidelines + This comment has been rejected for language that violates our + guidelines } diff --git a/client/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentRejectedMessage.tsx b/client/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentRejectedMessage.tsx index 196a663c52..b97848f848 100644 --- a/client/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentRejectedMessage.tsx +++ b/client/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentRejectedMessage.tsx @@ -20,7 +20,10 @@ const PostCommentRejected: FunctionComponent = ( icon={} title={ -
This comment has been rejected for violating our guidelines
+
+ This comment has been rejected for language that violates our + guidelines +
} onClose={props.onDismiss} diff --git a/client/src/core/client/stream/tabs/Profile/Preferences/MediaSettingsContainer.tsx b/client/src/core/client/stream/tabs/Profile/Preferences/MediaSettingsContainer.tsx index a3f75fbc59..278f9aa2b6 100644 --- a/client/src/core/client/stream/tabs/Profile/Preferences/MediaSettingsContainer.tsx +++ b/client/src/core/client/stream/tabs/Profile/Preferences/MediaSettingsContainer.tsx @@ -117,7 +117,7 @@ const MediaSettingsContainer: FunctionComponent = ({ variant="streamBlue" > -
Always show GIFs, Tweets, YouTube, etc.
+
Always show GIFs, X posts, YouTube, etc.
)} diff --git a/client/src/core/client/test/helpers/fixture.ts b/client/src/core/client/test/helpers/fixture.ts index 789770b1a6..7e04d122fa 100644 --- a/client/src/core/client/test/helpers/fixture.ts +++ b/client/src/core/client/test/helpers/fixture.ts @@ -50,6 +50,9 @@ export function createUserStatus(banned = false): GQLUserStatus { username: { history: [], }, + deletion: { + history: [], + }, premod: { active: false, history: [], diff --git a/client/src/core/client/ui/components/icons/SocialMediaTwitterIcon.tsx b/client/src/core/client/ui/components/icons/SocialMediaTwitterIcon.tsx deleted file mode 100644 index 3805d00661..0000000000 --- a/client/src/core/client/ui/components/icons/SocialMediaTwitterIcon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { FunctionComponent } from "react"; - -const SocialMediaTwitterIcon: FunctionComponent = () => { - // https://www.streamlinehq.com/icons/streamline-regular/logos/social-medias/social-media-twitter - return ( - - - - ); -}; - -export default SocialMediaTwitterIcon; diff --git a/client/src/core/client/ui/components/icons/XLogoTwitterIcon.tsx b/client/src/core/client/ui/components/icons/XLogoTwitterIcon.tsx new file mode 100644 index 0000000000..38929281cc --- /dev/null +++ b/client/src/core/client/ui/components/icons/XLogoTwitterIcon.tsx @@ -0,0 +1,29 @@ +import React, { FunctionComponent } from "react"; + +const XLogoTwitterIcon: FunctionComponent = () => { + // https://www.streamlinehq.com/icons/streamline-regular/logos/social-medias/x-logo-twitter + return ( + + + + + + ); +}; + +export default XLogoTwitterIcon; diff --git a/client/src/core/client/ui/components/icons/index.ts b/client/src/core/client/ui/components/icons/index.ts index e9994ed0ae..99cbb1677a 100644 --- a/client/src/core/client/ui/components/icons/index.ts +++ b/client/src/core/client/ui/components/icons/index.ts @@ -76,7 +76,6 @@ export { default as SingleNeutralActionsAddIcon } from "./SingleNeutralActionsAd export { default as SingleNeutralActionsBlockIcon } from "./SingleNeutralActionsBlockIcon"; export { default as SingleNeutralCircleIcon } from "./SingleNeutralCircleIcon"; export { default as SingleNeutralProfilePictureIcon } from "./SingleNeutralProfilePictureIcon"; -export { default as SocialMediaTwitterIcon } from "./SocialMediaTwitterIcon"; export { default as StopwatchIcon } from "./StopwatchIcon"; export { default as SvgIcon } from "./SvgIcon"; export { default as TextBoldIcon } from "./TextBoldIcon"; @@ -88,3 +87,4 @@ export { default as TradingConversationIcon } from "./TradingConversationIcon"; export { default as VideoPlayerIcon } from "./VideoPlayerIcon"; export { default as ViewIcon } from "./ViewIcon"; export { default as ViewOffIcon } from "./ViewOffIcon"; +export { default as XLogoTwitterIcon } from "./XLogoTwitterIcon"; diff --git a/common/lib/constants.ts b/common/lib/constants.ts index 5923bce392..db19d6d600 100644 --- a/common/lib/constants.ts +++ b/common/lib/constants.ts @@ -159,6 +159,13 @@ export const PROTECTED_EMAIL_DOMAINS = new Set([ "yahoo.no", "hotmail.no", "rr.com", + "outlook.co.nz", + "outlook.at", + "outlook.in", + "outlook.com.au", + "outlook.fr", + "outlook.de", + "outlook.jp", ]); export const FLAIR_BADGE_NAME_REGEX = "^[\\w.-]+$"; diff --git a/common/package-lock.json b/common/package-lock.json index 21127c64fa..b70606f3e9 100644 --- a/common/package-lock.json +++ b/common/package-lock.json @@ -1,12 +1,12 @@ { "name": "common", - "version": "8.7.1", + "version": "8.7.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "common", - "version": "8.7.1", + "version": "8.7.2", "license": "ISC", "dependencies": { "coral-config": "../config/dist", diff --git a/common/package.json b/common/package.json index bcd34a0574..ce2f0bce81 100644 --- a/common/package.json +++ b/common/package.json @@ -1,6 +1,6 @@ { "name": "common", - "version": "8.7.1", + "version": "8.7.2", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/config/package-lock.json b/config/package-lock.json index 783f636eeb..40693f40ed 100644 --- a/config/package-lock.json +++ b/config/package-lock.json @@ -1,12 +1,12 @@ { "name": "common", - "version": "8.7.1", + "version": "8.7.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "common", - "version": "8.7.1", + "version": "8.7.2", "license": "ISC", "dependencies": { "typescript": "^3.9.5" diff --git a/config/package.json b/config/package.json index b0a15d9683..3a0d00c41f 100644 --- a/config/package.json +++ b/config/package.json @@ -1,6 +1,6 @@ { "name": "common", - "version": "8.7.1", + "version": "8.7.2", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/locales/en-US/admin.ftl b/locales/en-US/admin.ftl index 2b1dab61d4..eff09ec811 100644 --- a/locales/en-US/admin.ftl +++ b/locales/en-US/admin.ftl @@ -434,8 +434,8 @@ configure-general-sitewideCommenting-messageExplanation = #### Embed Links configure-general-embedLinks-title = Embedded media -configure-general-embedLinks-desc = Allow commenters to add a YouTube video, Tweet or GIF from GIPHY's library to the end of their comment -configure-general-embedLinks-enableTwitterEmbeds = Allow Twitter embeds +configure-general-embedLinks-desc = Allow commenters to add a YouTube video, X post or GIF from GIPHY's library to the end of their comment +configure-general-embedLinks-enableTwitterEmbeds = Allow X post embeds configure-general-embedLinks-enableYouTubeEmbeds = Allow YouTube embeds configure-general-embedLinks-enableGiphyEmbeds = Allow GIFs from GIPHY configure-general-embedLinks-enableExternalEmbeds = Enable external media @@ -1232,6 +1232,33 @@ moderate-user-drawer-suspension = *[other] unknown unit } +moderate-user-drawer-deleteAccount-popover = + .description = A popover menu to delete a user's account +moderate-user-drawer-deleteAccount-button = + .aria-label = Delete account +moderate-user-drawer-deleteAccount-popover-confirm = Type in "{ $text }" to confirm +moderate-user-drawer-deleteAccount-popover-title = Delete account +moderate-user-drawer-deleteAccount-popover-username = Username +moderate-user-drawer-deleteAccount-popover-header-description = Delete account will +moderate-user-drawer-deleteAccount-popover-description-list-removeComments = Remove all comments written by this user from the database. +moderate-user-drawer-deleteAccount-popover-description-list-deleteAll = Delete all record of this account. The + user could then create a new account using the same email address. If you want to Ban this user instead and + retain their history, press "CANCEL" and use the Status dropdown below the username. +moderate-user-drawer-deleteAccount-popover-callout = This removes all records of this user +moderate-user-drawer-deleteAccount-popover-timeframe = This will go into effect in 24 hours. +moderate-user-drawer-deleteAccount-popover-cancelButton = Cancel +moderate-user-drawer-deleteAccount-popover-deleteButton = Delete + +moderate-user-drawer-deleteAccount-scheduled-callout = User deletion activated +moderate-user-drawer-deleteAccount-scheduled-timeframe = This will occur at { $deletionDate }. +moderate-user-drawer-deleteAccount-scheduled-cancelDeletion = Cancel user deletion + +moderate-user-drawer-user-scheduled-deletion = User scheduled for deletion +moderate-user-drawer-user-deletion-canceled = User deletion request canceled + +moderate-user-drawer-account-history-deletion-scheduled = Deletion scheduled for { $createdAt } +moderate-user-drawer-account-history-canceled-at = Canceled at { $createdAt } +moderate-user-drawer-account-history-updated-at = Updated at { $createdAt } moderate-user-drawer-recent-history-title = Recent comment history moderate-user-drawer-recent-history-calculated = diff --git a/locales/en-US/stream.ftl b/locales/en-US/stream.ftl index 9fda6fbfdd..a47960590c 100644 --- a/locales/en-US/stream.ftl +++ b/locales/en-US/stream.ftl @@ -127,9 +127,9 @@ comments-postComment-pasteImage = Paste image URL comments-postComment-insertImage = Insert comments-postComment-confirmMedia-youtube = Add this YouTube video to the end of your comment? -comments-postComment-confirmMedia-twitter = Add this Tweet to the end of your comment? +comments-postComment-confirmMedia-twitter = Add this post to the end of your comment? comments-postComment-confirmMedia-cancel = Cancel -comments-postComment-confirmMedia-add-tweet = Add Tweet +comments-postComment-confirmMedia-add-tweet = Add post comments-postComment-confirmMedia-add-video = Add video comments-postComment-confirmMedia-remove = Remove comments-commentForm-gifPreview-remove = Remove @@ -467,8 +467,8 @@ comments-embedLinks-hide-giphy = Hide GIF comments-embedLinks-show-youtube = Show video comments-embedLinks-hide-youtube = Hide video -comments-embedLinks-show-twitter = Show Tweet -comments-embedLinks-hide-twitter = Hide Tweet +comments-embedLinks-show-twitter = Show post +comments-embedLinks-hide-twitter = Hide post comments-embedLinks-show-external = Show image comments-embedLinks-hide-external = Hide image @@ -564,7 +564,7 @@ profile-commentHistory-archived-thisIsAllYourComments = ### Preferences profile-preferences-mediaPreferences = Media Preferences -profile-preferences-mediaPreferences-alwaysShow = Always show GIFs, Tweets, YouTube, etc. +profile-preferences-mediaPreferences-alwaysShow = Always show GIFs, X posts, YouTube, etc. profile-preferences-mediaPreferences-thisMayMake = This may make the comments slower to load profile-preferences-mediaPreferences-update = Update profile-preferences-mediaPreferences-preferencesUpdated = @@ -780,7 +780,7 @@ comments-submitStatus-dismiss = Dismiss comments-submitStatus-submittedAndWillBeReviewed = Your comment has been submitted and will be reviewed by a moderator comments-submitStatus-submittedAndRejected = - This comment has been rejected for violating our guidelines + This comment has been rejected for language that violates our guidelines # Configure configure-configureQuery-errorLoadingProfile = Error loading configure diff --git a/locales/nl-NL/account.ftl b/locales/nl-NL/account.ftl index f79256a416..aec4da579f 100644 --- a/locales/nl-NL/account.ftl +++ b/locales/nl-NL/account.ftl @@ -26,15 +26,17 @@ resetPassword-missingResetToken = De markering voor herstel lijkt te ontbreken. ## Email Confirmation +confirmEmail-emailConfirmation = Bevestigings-e-mail confirmEmail-confirmYourEmailAddress = Bevestig je e-mailadres -confirmEmail-confirmEmail = Bevestig e-mail +confirmEmail-confirmEmail = + Bevestig e-mail confirmEmail-pleaseClickToConfirm = Klik onderaan om je e-mailadres te bevestigen. confirmEmail-oopsSorry = Oeps sorry! -confirmEmail-missingConfirmToken = De markering voor bevestiging lijkt te ontbreken. +confirmEmail-missingConfirmToken = De token voor bevestiging lijkt te ontbreken. confirmEmail-successfullyConfirmed = E-mail met succes bevestigd confirmEmail-youMayClose = - Je mag dit venster nu sluiten. + Je kunt dit venster nu sluiten. ## Download @@ -48,16 +50,20 @@ download-landingPage-contentsDescription = download-landingPage-contentsDate = Wanneer je de reactie schreef download-landingPage-contentsUrl = - De volledige URL voor de reactie + De volledige URL van de reactie download-landingPage-contentsText = De tekst van de reactie download-landingPage-contentsStoryUrl = De URL van het artikel waar de reactie verschijnt +download-landingPage-downloadComments = Reacties downloaden download-landingPage-download = Download download-landingPage-sorry = Je download link is ongeldig. ## Unsubscribe +unsubscribe-confirm = Bevestigen +unsubscribe-successfullyUnsubscribed = Je bent succesvol afgemeld voor meldingen. + unsubscribe-unsubscribeFromEmails = Uitschrijven voor e-mailmeldingen unsubscribe-oopsSorry = Oeps sorry! unsubscribe-clickToConfirm = @@ -66,4 +72,4 @@ unsubscribe-submit-unsubscribe = Uitschrijven unsubscribe-unsubscribedSuccessfully = Succesvol afgemeld voor e-mailmeldingen unsubscribe-youMayNowClose = - Je mag dit venster nu sluiten. + Je kunt dit venster nu sluiten. \ No newline at end of file diff --git a/locales/nl-NL/common.ftl b/locales/nl-NL/common.ftl index c0624ddf6e..18052370e7 100644 --- a/locales/nl-NL/common.ftl +++ b/locales/nl-NL/common.ftl @@ -19,7 +19,49 @@ common-experimentalTag-tooltip-title = Experimentele functie common-error-title = Er is een fout opgetreden common-error-message = Bericht -common-error-traceID = Traceer-ID +common-error-traceID = Trace ID common-username = .aria-label = Gebruiker { $username } + +common-moderationReason-reason = + Reden +common-moderationReason-addExplanation = + Uitleg toevoegen +common-moderationReason-reject = + Afwijzen +common-moderationReason-cancel = + Annuleren +common-moderationReason-rejectionReason-OFFENSIVE = + Aanstootgevend +common-moderationReason-rejectionReason-ABUSIVE = + Beledigend +common-moderationReason-rejectionReason-SPAM = + Spam +common-moderationReason-rejectionReason-BANNED_WORD = + Verboden woord +common-moderationReason-rejectionReason-AD = + Advertentie +common-moderationReason-rejectionReason-HARASSMENT_BULLYING = + Intimidatie / pesten +common-moderationReason-rejectionReason-MISINFORMATION = + Desinformatie +common-moderationReason-rejectionReason-HATE_SPEECH = + Haatdragend +common-moderationReason-rejectionReason-IRRELEVANT_CONTENT = + Irrelevante inhoud +common-moderationReason-rejectionReason-OTHER = + Overig +common-moderationReason-changeReason = + < Reden wijzigen +common-moderationReason-reasonLabel = Reden +common-moderationReason-detailedExplanation = + Gedetailleerde uitleg +common-moderationReason-detailedExplanation-placeholder = + .placeholder = Voeg je uitleg toe +common-moderationReason-customReason = Aangepaste reden (vereist) +common-moderationReason-customReason-placeholder = + .placeholder = Voeg je reden toe + +common-userBanned = + Gebruiker is verbannen. \ No newline at end of file diff --git a/locales/nl-NL/stream.ftl b/locales/nl-NL/stream.ftl index 2ee529a239..b058695246 100644 --- a/locales/nl-NL/stream.ftl +++ b/locales/nl-NL/stream.ftl @@ -978,3 +978,62 @@ stream-footer-links-discussions = Meer discussies .title = Ga naar meer discussies stream-footer-navigation = .aria-label = Footer reacties + +## Notifications + +notifications-title = Notificaties +notifications-loadMore = Meer laden + +notification-comment-toggle-default-open = - Reactie +notification-comment-toggle-default-closed = + Reactie + +notifications-comment-showRemovedComment = + Verwijderde reactie tonen +notifications-comment-hideRemovedComment = - Verwijderde reactie verbergen + +notifications-yourIllegalContentReportHasBeenReviewed = + Je melding van illegale inhoud is beoordeeld +notifications-yourCommentHasBeenRejected = + Je reactie is afgewezen +notifications-yourCommentHasBeenApproved = + Je reactie is goedgekeurd +notifications-yourCommentHasBeenFeatured = + Je reactie is uitgelicht +notifications-defaultTitle = Notificatie + +notifications-rejectedComment-body = + Je reactie was in strijd met onze huisregels. De reactie is verwijderd. +notifications-reasonForRemoval = Reden voor verwijdering +notifications-legalGrounds = Juridische gronden +notifications-additionalExplanation = Aanvullende uitleg + +notifications-dsaReportLegality-legal = Legale inhoud +notifications-dsaReportLegality-illegal = Illegale inhoud +notifications-dsaReportLegality-unknown = Onbekend + +notifications-rejectionReason-offensive = Deze reactie bevat aanstootgevende taal +notifications-rejectionReason-abusive = Deze reactie bevat beledigende taal +notifications-rejectionReason-spam = Deze reactie is spam +notifications-rejectionReason-bannedWord = Verboden woord +notifications-rejectionReason-ad = Deze reactie is een advertentie +notifications-rejectionReason-illegalContent = Deze reactie bevat illegale inhoud +notifications-rejectionReason-harassmentBullying = Deze reactie bevat intimderend taalgebruik of pestengedrag +notifications-rejectionReason-misinformation = Deze reactie bevat desinformatie +notifications-rejectionReason-hateSpeech = Deze reactie bevat haatdragende taal +notifications-rejectionReason-irrelevant = Deze reactie is irrelevant voor de discussie +notifications-rejectionReason-other = Overig +notifications-rejectionReason-other-customReason = Overig - { $customReason } +notifications-rejectionReason-unknown = Onbekend + +notifications-reportDecisionMade-legal = + Op { $date } heb je een reactie gemeld van { $author } wegens het bevatten van illegale inhoud. Na beoordeling van jouw melding heeft ons moderatieteam besloten dat deze reactie geen illegale inhoud lijkt te bevatten. Bedankt voor je hulp bij het veilig houden van onze community. +notifications-reportDecisionMade-illegal = + Op { $date } heb je een reactie gemeld van { $author } wegens het bevatten van illegale inhoud. Na beoordeling van jouw melding heeft ons moderatieteam besloten dat deze reactie inderdaad illegale inhoud bevat en is verwijderd. Er kunnen verdere stappen ondernomen worden tegen degene die de reactie heeft geplaatst, maar daar krijg je geen meldingen van. Bedankt voor je hulp bij het veilig houden van onze community. + +notifications-methodOfRedress-none = + Alle moderatiebeslissingen zijn definitief en bezwaar is niet mogelijk +notifications-methodOfRedress-email = + Om bezwaar te maken tegen een beslissing die hier verschijnt, neem contact op via { $email } +notifications-methodOfRedress-url = + Om bezwaar te maken tegen een beslissing die hier verschijnt, bezoek { $url } + +notifications-youDoNotCurrentlyHaveAny = Je hebt momenteel geen notificaties diff --git a/server/package-lock.json b/server/package-lock.json index d1be7599aa..8ef4d65674 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@coralproject/talk", - "version": "8.7.1", + "version": "8.7.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@coralproject/talk", - "version": "8.7.1", + "version": "8.7.2", "license": "Apache-2.0", "dependencies": { "@ampproject/toolbox-cache-url": "^2.9.0", diff --git a/server/package.json b/server/package.json index 85c225268a..0e1736fb2c 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@coralproject/talk", - "version": "8.7.1", + "version": "8.7.2", "author": "The Coral Project", "homepage": "https://coralproject.net/", "sideEffects": [ diff --git a/server/src/core/server/app/router/client.ts b/server/src/core/server/app/router/client.ts index 7f3335d2ce..704bf85393 100644 --- a/server/src/core/server/app/router/client.ts +++ b/server/src/core/server/app/router/client.ts @@ -163,6 +163,7 @@ const clientHandler = defaultLocale, template: viewTemplate = "client", templateVariables = {}, + config, }: ClientTargetHandlerOptions): RequestHandler => async (req, res, next) => { // Grab the locale code from the tenant configuration, if available. @@ -173,6 +174,12 @@ const clientHandler = rootURL = `${req.protocol}://${req.coral.tenant?.domain}`; } + // this supports local wordpress plugin development + if (config.get("env") === "development") { + const port = config.get("port"); + rootURL = `${req.protocol}://${req.coral.tenant?.domain}:${port}`; + } + const entrypoint = await entrypointLoader(); if (!entrypoint) { next(new Error("Entrypoint not available")); diff --git a/server/src/core/server/graph/mutators/Users.ts b/server/src/core/server/graph/mutators/Users.ts index 141b938909..59cdf6771f 100644 --- a/server/src/core/server/graph/mutators/Users.ts +++ b/server/src/core/server/graph/mutators/Users.ts @@ -8,6 +8,7 @@ import { addModeratorNote, ban, cancelAccountDeletion, + cancelScheduledAccountDeletion, createToken, deactivateToken, demoteMember, @@ -25,6 +26,7 @@ import { requestAccountDeletion, requestCommentsDownload, requestUserCommentsDownload, + scheduleAccountDeletion, sendModMessage, setEmail, setPassword, @@ -52,6 +54,7 @@ import { deleteUser } from "coral-server/services/users/delete"; import { GQLBanUserInput, GQLCancelAccountDeletionInput, + GQLCancelScheduledAccountDeletionInput, GQLCreateModeratorNoteInput, GQLCreateTokenInput, GQLDeactivateTokenInput, @@ -72,6 +75,7 @@ import { GQLRequestAccountDeletionInput, GQLRequestCommentsDownloadInput, GQLRequestUserCommentsDownloadInput, + GQLScheduleAccountDeletionInput, GQLSendModMessageInput, GQLSetEmailInput, GQLSetPasswordInput, @@ -174,6 +178,17 @@ export const Users = (ctx: GraphContext) => ({ ), { "input.password": [ERROR_CODES.PASSWORD_INCORRECT] } ), + scheduleAccountDeletion: async ( + input: GQLScheduleAccountDeletionInput + ): Promise | null> => + scheduleAccountDeletion( + ctx.mongo, + ctx.mailerQueue, + ctx.tenant, + ctx.user!, + input.userID, + ctx.now + ), deleteAccount: async ( input: GQLDeleteUserAccountInput ): Promise | null> => { @@ -189,9 +204,20 @@ export const Users = (ctx: GraphContext) => ({ input.userID, ctx.tenant.id, ctx.now, - ctx.tenant.dsa?.enabled + ctx.tenant.dsa?.enabled, + ctx.user!.id ); }, + cancelScheduledAccountDeletion: async ( + input: GQLCancelScheduledAccountDeletionInput + ): Promise | null> => + cancelScheduledAccountDeletion( + ctx.mongo, + ctx.mailerQueue, + ctx.tenant, + ctx.user!, + input.userID + ), cancelAccountDeletion: async ( input: GQLCancelAccountDeletionInput ): Promise | null> => diff --git a/server/src/core/server/graph/resolvers/Mutation.ts b/server/src/core/server/graph/resolvers/Mutation.ts index 26e23ffb2d..e5ac47184d 100644 --- a/server/src/core/server/graph/resolvers/Mutation.ts +++ b/server/src/core/server/graph/resolvers/Mutation.ts @@ -307,6 +307,14 @@ export const Mutation: Required> = { user: await ctx.mutators.Users.requestAccountDeletion(input), clientMutationId: input.clientMutationId, }), + scheduleAccountDeletion: async (source, { input }, ctx) => ({ + user: await ctx.mutators.Users.scheduleAccountDeletion(input), + clientMutationId: input.clientMutationId, + }), + cancelScheduledAccountDeletion: async (source, { input }, ctx) => ({ + user: await ctx.mutators.Users.cancelScheduledAccountDeletion(input), + clientMutationId: input.clientMutationId, + }), cancelAccountDeletion: async (source, { input }, ctx) => ({ user: await ctx.mutators.Users.cancelAccountDeletion(input), clientMutationId: input.clientMutationId, diff --git a/server/src/core/server/graph/resolvers/UserDeletionHistory.ts b/server/src/core/server/graph/resolvers/UserDeletionHistory.ts new file mode 100644 index 0000000000..c14d53cc94 --- /dev/null +++ b/server/src/core/server/graph/resolvers/UserDeletionHistory.ts @@ -0,0 +1,17 @@ +import * as user from "coral-server/models/user"; + +import { GQLUserDeletionHistoryTypeResolver } from "coral-server/graph/schema/__generated__/types"; + +export const UserDeletionHistory: Required< + GQLUserDeletionHistoryTypeResolver +> = { + createdBy: ({ createdBy }, input, ctx) => { + if (createdBy) { + return ctx.loaders.Users.user.load(createdBy); + } + + return null; + }, + createdAt: ({ createdAt }) => createdAt, + updateType: ({ updateType }) => updateType, +}; diff --git a/server/src/core/server/graph/resolvers/UserDeletionStatus.ts b/server/src/core/server/graph/resolvers/UserDeletionStatus.ts new file mode 100644 index 0000000000..ac90b84531 --- /dev/null +++ b/server/src/core/server/graph/resolvers/UserDeletionStatus.ts @@ -0,0 +1,14 @@ +import * as user from "coral-server/models/user"; + +import { GQLUserDeletionStatusTypeResolver } from "coral-server/graph/schema/__generated__/types"; + +export type UserDeletionStatusInput = user.ConsolidatedUserDeletionStatus & { + userID: string; +}; + +export const UserDeletionStatus: Required< + GQLUserDeletionStatusTypeResolver +> = { + history: ({ history, userID }) => + history.map((status) => ({ ...status, userID })), +}; diff --git a/server/src/core/server/graph/resolvers/UserStatus.ts b/server/src/core/server/graph/resolvers/UserStatus.ts index 85a8201492..8daeeac520 100644 --- a/server/src/core/server/graph/resolvers/UserStatus.ts +++ b/server/src/core/server/graph/resolvers/UserStatus.ts @@ -9,6 +9,7 @@ import { BanStatusInput } from "./BanStatus"; import { ModMessageStatusInput } from "./ModMessageStatus"; import { PremodStatusInput } from "./PremodStatus"; import { SuspensionStatusInput } from "./SuspensionStatus"; +import { UserDeletionStatusInput } from "./UserDeletionStatus"; import { UsernameStatusInput } from "./UsernameStatus"; import { WarningStatusInput } from "./WarningStatus"; @@ -57,6 +58,10 @@ export const UserStatus: Required> = ...user.consolidateUsernameStatus(username), userID, }), + deletion: ({ userID, deletion }): UserDeletionStatusInput => ({ + ...user.consolidateUserDeletionStatus(deletion), + userID, + }), ban: async ({ ban, userID }, args, ctx): Promise => ({ ...user.consolidateUserBanStatus(ban, ctx.site?.id), userID, diff --git a/server/src/core/server/graph/resolvers/index.ts b/server/src/core/server/graph/resolvers/index.ts index 5206a6d196..9e927b4f4a 100644 --- a/server/src/core/server/graph/resolvers/index.ts +++ b/server/src/core/server/graph/resolvers/index.ts @@ -78,6 +78,8 @@ import { SuspensionStatusHistory } from "./SuspensionStatusHistory"; import { Tag } from "./Tag"; import { TwitterMediaConfiguration } from "./TwitterMediaConfiguration"; import { User } from "./User"; +import { UserDeletionHistory } from "./UserDeletionHistory"; +import { UserDeletionStatus } from "./UserDeletionStatus"; import { UserMediaSettings } from "./UserMediaSettings"; import { UserMembershipScopes } from "./UserMembershipScopes"; import { UserModerationScopes } from "./UserModerationScopes"; @@ -160,6 +162,8 @@ const Resolvers: GQLResolver = { Time, TwitterMediaConfiguration, User, + UserDeletionHistory, + UserDeletionStatus, UserMediaSettings, UserMembershipScopes, UserModerationScopes, diff --git a/server/src/core/server/graph/schema/schema.graphql b/server/src/core/server/graph/schema/schema.graphql index 3b9129a7cb..7b42ef6e73 100644 --- a/server/src/core/server/graph/schema/schema.graphql +++ b/server/src/core/server/graph/schema/schema.graphql @@ -2710,6 +2710,33 @@ type UsernameStatus { history: [UsernameHistory!]! } +enum UserDeletionUpdateType { + REQUESTED + CANCELED +} + +type UserDeletionHistory { + """ + updateType is the type of deletion status update that was made. For example, + user deletion was requested or canceled. + """ + updateType: UserDeletionUpdateType! + + """ + createdBy is the user that made the deletion status update. + """ + createdBy: User + + """ + createdAt is the time the user had a deletion status update. + """ + createdAt: Time! +} + +type UserDeletionStatus { + history: [UserDeletionHistory!]! @auth(roles: [ADMIN, MODERATOR]) +} + """ UserStatus stores the user status information regarding moderation state. """ @@ -2734,6 +2761,16 @@ type UserStatus { permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED] ) + """ + deletion stores the history of deletion requests and cancellations for the user + """ + deletion: UserDeletionStatus! + @auth( + userIDField: "userID" + roles: [ADMIN, MODERATOR] + permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED] + ) + """ banned stores the user banned status as well as the history of changes. """ @@ -8297,6 +8334,62 @@ type RequestAccountDeletionPayload { clientMutationId: String! } +################## +# scheduleAccountDeletion +################## + +input ScheduleAccountDeletionInput { + """ + userID is the ID of the User being deleted. + """ + userID: ID! + + """ + clientMutationId is required for Relay support. + """ + clientMutationId: String! +} + +type ScheduleAccountDeletionPayload { + """ + user is the User that was deleted. + """ + user: User + + """ + clientMutationId is required for Relay support. + """ + clientMutationId: String! +} + +################## +# cancelScheduledAccountDeletion +################## + +input CancelScheduledAccountDeletionInput { + """ + userID is the ID of the User whose scheduled deletion is being canceled. + """ + userID: ID! + + """ + clientMutationId is required for Relay support. + """ + clientMutationId: String! +} + +type CancelScheduledAccountDeletionPayload { + """ + user is the User whose scheduled deletion was canceled. + """ + user: User + + """ + clientMutationId is required for Relay support. + """ + clientMutationId: String! +} + ################## # deleteUserAccount ################## @@ -10278,6 +10371,22 @@ type Mutation { @auth(permit: [SUSPENDED, BANNED, WARNED]) @rate(seconds: 10) + """ + scheduleAccountDeletion allows an admin to schedule an account deletion + for a target user + """ + scheduleAccountDeletion( + input: ScheduleAccountDeletionInput! + ): ScheduleAccountDeletionPayload! @auth(roles: [ADMIN]) + + """ + cancelScheduledAccountDeletion allows an admin to cancel a scheduled account deletion + for a target user + """ + cancelScheduledAccountDeletion( + input: CancelScheduledAccountDeletionInput! + ): CancelScheduledAccountDeletionPayload! @auth(roles: [ADMIN]) + """ deleteUserAccount will delete the target user now. """ diff --git a/server/src/core/server/models/user/user.ts b/server/src/core/server/models/user/user.ts index 519dc6478b..ade988ef19 100644 --- a/server/src/core/server/models/user/user.ts +++ b/server/src/core/server/models/user/user.ts @@ -39,6 +39,8 @@ import { GQLSuspensionStatus, GQLTimeRange, GQLUSER_ROLE, + GQLUserDeletionStatus, + GQLUserDeletionUpdateType, GQLUserMediaSettings, GQLUsernameStatus, GQLUserNotificationSettings, @@ -273,6 +275,37 @@ export interface UsernameStatus { history: UsernameHistory[]; } +export interface UserDeletionHistory { + /** + * id is a specific reference for a particular user deletion history that will be + * used internally to update user deletion records. + */ + id: string; + + /** + * updateType is the kind of update to a user's deletion status that was made, + * whether it was requested or canceled. + */ + updateType: GQLUserDeletionUpdateType; + + /** + * createdBy is the user that made this deletion status update + */ + createdBy: string; + + /** + * createdAt is when the deletion status update was made + */ + createdAt: string; +} + +export interface UserDeletionStatus { + /** + * history is the list of all user deletion status updates for this user + */ + history: UserDeletionHistory[]; +} + /** * PremodStatusHistory is the history of premod status changes * against a specific User. @@ -406,6 +439,11 @@ export interface UserStatus { * a history of moderation messages */ modMessage?: ModMessageStatus; + + /** + * deletion stores the history of deletion status updates for a user. + */ + deletion: UserDeletionStatus; } /** @@ -675,6 +713,9 @@ export async function findOrCreateUserInput( premod: { active: false, history: [] }, warning: { active: false, history: [] }, modMessage: { active: false, history: [] }, + deletion: { + history: [], + }, }, notifications: { onReply: false, @@ -1158,9 +1199,18 @@ export async function updateUserPassword( export async function scheduleDeletionDate( mongo: MongoContext, tenantID: string, + requestingUserID: string, userID: string, - deletionDate: Date + deletionDate: Date, + now = new Date() ) { + const scheduleDeletionHistory = { + id: uuid(), + createdBy: requestingUserID, + createdAt: now, + updateType: GQLUserDeletionUpdateType.REQUESTED, + }; + const result = await mongo.users().findOneAndUpdate( { id: userID, @@ -1170,6 +1220,9 @@ export async function scheduleDeletionDate( $set: { scheduledDeletionDate: deletionDate, }, + $push: { + "status.deletion.history": scheduleDeletionHistory, + }, }, { returnOriginal: false, @@ -1185,8 +1238,16 @@ export async function scheduleDeletionDate( export async function clearDeletionDate( mongo: MongoContext, tenantID: string, - userID: string + userID: string, + requestingUserID: string, + now = new Date() ) { + const cancelDeletionHistory = { + id: uuid(), + createdBy: requestingUserID, + createdAt: now, + updateType: GQLUserDeletionUpdateType.CANCELED, + }; const result = await mongo.users().findOneAndUpdate( { id: userID, @@ -1196,6 +1257,9 @@ export async function clearDeletionDate( $unset: { scheduledDeletionDate: "", }, + $push: { + "status.deletion.history": cancelDeletionHistory, + }, }, { // We want to return edited user so that @@ -2643,6 +2707,12 @@ export type ConsolidatedBanStatus = Omit & export type ConsolidatedUsernameStatus = Omit & Pick; +export type ConsolidatedUserDeletionStatus = Omit< + GQLUserDeletionStatus, + "history" +> & + Pick; + export type ConsolidatedPremodStatus = Omit & Pick; @@ -2661,6 +2731,17 @@ export function consolidateUsernameStatus( return username; } +export function consolidateUserDeletionStatus( + deletion: User["status"]["deletion"] +) { + if (!deletion) { + return { + history: [], + }; + } + return deletion; +} + const computeBanActive = (ban: BanStatus, siteID?: string) => { if (ban.active) { return true; diff --git a/server/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-cancel.html b/server/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-cancel.html index 99b4f900d5..d548f04f76 100644 --- a/server/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-cancel.html +++ b/server/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-cancel.html @@ -1,5 +1,5 @@ {% extends "layouts/account-notification.html" %} {% block content %} - You have cancelled your account deletion request for {{ context.organizationName }}. Your account is now reactivated. + The account deletion request for {{ context.organizationName }} has been cancelled. Your account is now reactivated. {% endblock %} diff --git a/server/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-confirmation.html b/server/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-confirmation.html index ea86a3fce2..6e089f6165 100644 --- a/server/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-confirmation.html +++ b/server/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-confirmation.html @@ -1,9 +1,9 @@ {% extends "layouts/account-notification.html" %} {% block content %} - A request to delete your commenter account was received. Your account is scheduled for deletion on {{ context.requestDate }}. + We have received a request to delete your commenter account. Your account is scheduled for deletion on {{ context.requestDate }}. After that time all of your comments will be removed from the site, all of your comments will be removed from our database, and your username and email address will be removed from our system. - If you change your mind you can sign into your account and cancel the request before your scheduled account deletion time. + If you would like to cancel the request, you can sign into your account and cancel the request before your scheduled account deletion time. {% endblock %} diff --git a/server/src/core/server/services/users/delete.ts b/server/src/core/server/services/users/delete.ts index 0284eeaf2c..822317ffa7 100644 --- a/server/src/core/server/services/users/delete.ts +++ b/server/src/core/server/services/users/delete.ts @@ -365,7 +365,8 @@ export async function deleteUser( userID: string, tenantID: string, now: Date, - dsaEnabled: boolean + dsaEnabled: boolean, + requestingUser: string | null = null ) { const user = await mongo.users().findOne({ id: userID, tenantID }); if (!user) { diff --git a/server/src/core/server/services/users/users.ts b/server/src/core/server/services/users/users.ts index a14c69524f..81cc078c6b 100644 --- a/server/src/core/server/services/users/users.ts +++ b/server/src/core/server/services/users/users.ts @@ -567,6 +567,7 @@ export async function requestAccountDeletion( mongo, tenant.id, user.id, + user.id, deletionDate.toJSDate() ); @@ -590,6 +591,82 @@ export async function requestAccountDeletion( return updatedUser; } +export async function scheduleAccountDeletion( + mongo: MongoContext, + mailer: MailerQueue, + tenant: Tenant, + requestingUser: User, + userID: string, + now: Date +) { + const deletionDate = DateTime.fromJSDate(now).plus({ + seconds: SCHEDULED_DELETION_WINDOW_DURATION, + }); + + const updatedUser = await scheduleDeletionDate( + mongo, + tenant.id, + requestingUser.id, + userID, + deletionDate.toJSDate(), + now + ); + + const formattedDate = formatDate(deletionDate.toJSDate(), tenant.locale); + + if (updatedUser.email) { + await mailer.add({ + tenantID: tenant.id, + message: { + to: updatedUser.email, + }, + template: { + name: "account-notification/delete-request-confirmation", + context: { + requestDate: formattedDate, + organizationName: tenant.organization.name, + organizationURL: tenant.organization.url, + }, + }, + }); + } + + return updatedUser; +} + +export async function cancelScheduledAccountDeletion( + mongo: MongoContext, + mailer: MailerQueue, + tenant: Tenant, + requestingUser: User, + userID: string +) { + const updatedUser = await clearDeletionDate( + mongo, + tenant.id, + userID, + requestingUser.id + ); + + if (updatedUser.email) { + await mailer.add({ + tenantID: tenant.id, + message: { + to: updatedUser.email, + }, + template: { + name: "account-notification/delete-request-cancel", + context: { + organizationName: tenant.organization.name, + organizationURL: tenant.organization.url, + }, + }, + }); + } + + return updatedUser; +} + export async function cancelAccountDeletion( mongo: MongoContext, mailer: MailerQueue, @@ -600,7 +677,12 @@ export async function cancelAccountDeletion( throw new EmailNotSetError(); } - const updatedUser = await clearDeletionDate(mongo, tenant.id, user.id); + const updatedUser = await clearDeletionDate( + mongo, + tenant.id, + user.id, + user.id + ); await mailer.add({ tenantID: tenant.id, diff --git a/server/src/core/server/test/fixtures.ts b/server/src/core/server/test/fixtures.ts index c47e3b54aa..7773df0de5 100644 --- a/server/src/core/server/test/fixtures.ts +++ b/server/src/core/server/test/fixtures.ts @@ -248,6 +248,9 @@ export const createUserFixture = (defaults: Defaults = {}): User => { siteIDs: [], history: [], }, + deletion: { + history: [], + }, username: { history: [ { diff --git a/wordpress/README.md b/wordpress/README.md new file mode 100644 index 0000000000..99c28f8280 --- /dev/null +++ b/wordpress/README.md @@ -0,0 +1,37 @@ +## Wordpress Dev Environment + +This is a dev environment for testing out the [talk-wp-plugin](https://github.com/coralproject/talk-wp-plugin) with Coral. + +### Usage + +- Spin up the mysql and wordpress containers: + + ``` + cd wordpress + docker-compose up + ``` + +- Close the terminal after docker-compose runs + +- Spin up Coral and add `http://localhost:8081` to a new or existing site so the new Wordpress URL is allowed. + +- Navigate to `http://localhost:8081`. + +- Follow the steps to create a new admin user for the wordpress deployment. + +- Install the plugin from [talk-wp-plugin](https://github.com/coralproject/talk-wp-plugin) by downloading the source code and zipping it up into a `.zip` archive. + +- Navigate to http://localhost:8081/wp-admin/plugins.php and click `Add New Plugin`. + +- From there, click `Upload Plugin`. + +- Point it to the `talk-wp-plugin` archive you created from its source code. + +- Enable the Coral plugin in the `Installed Plugins` list if it is not already enabled. + +- Go to `Settings > Coral Settings` and set the `Server Base URL` to http://localhost:3000 or http://localhost:8080 based on whether you're running Coral standalone or in watch mode. + +- Set the Coral stream version to `v5+`. + +- Head to `Appearance > Themes` and select the oldest theme you can find (Twenty Twenty-Two as of this writing) as the Coral plugin can't override the PHP for comments in newer themes yet. + - If you need to set this manually, check the [README](https://github.com/coralproject/talk-wp-plugin?tab=readme-ov-file#theme-usage) on the `talk-wp-plugin` repo for how to [edit the theme to show Coral comments](https://github.com/coralproject/talk-wp-plugin?tab=readme-ov-file#theme-usage). \ No newline at end of file diff --git a/wordpress/docker-compose.yml b/wordpress/docker-compose.yml new file mode 100644 index 0000000000..c62af64fb8 --- /dev/null +++ b/wordpress/docker-compose.yml @@ -0,0 +1,21 @@ +services: + wp-mysql: + image: mysql:8.2.0 + restart: always + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD=wp-mysql-secret + - MYSQL_DATABASE=wp + - MYSQL_USER=wp + - MYSQL_PASSWORD=wp-user-secret + wp: + image: wordpress:6.4.2-php8.1-apache + restart: always + ports: + - "8081:80" + environment: + - WORDPRESS_DB_HOST=wp-mysql + - WORDPRESS_DB_NAME=wp + - WORDPRESS_DB_USER=wp + - WORDPRESS_DB_PASSWORD=wp-user-secret