From 97cd8de1dd83141207a6aa0332457830811c3c9f Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 4 Jan 2024 15:51:15 -0500 Subject: [PATCH 01/28] show delete account button in user account history if viewer is admin --- .../UserDrawerAccountHistory.tsx | 19 ++++++++++++++++++- .../UserDrawerAccountHistoryQuery.tsx | 7 ++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx index 4446d57386..98d20649d8 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx @@ -4,8 +4,10 @@ import { graphql } from "react-relay"; import { useDateTimeFormatter } from "coral-framework/hooks"; import { withFragmentContainer } from "coral-framework/lib/relay"; +import { GQLUSER_ROLE } from "coral-framework/schema"; import { CoralMarkIcon, SvgIcon } from "coral-ui/components/icons"; import { + Button, CallOut, HorizontalGutter, Table, @@ -16,6 +18,7 @@ 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, @@ -26,6 +29,7 @@ import styles from "./UserDrawerAccountHistory.css"; interface Props { user: UserDrawerAccountHistory_user; + viewer: UserDrawerAccountHistory_viewer; } interface From { @@ -39,7 +43,10 @@ type HistoryRecord = HistoryActionProps & { description?: string | null; }; -const UserDrawerAccountHistory: FunctionComponent = ({ user }) => { +const UserDrawerAccountHistory: FunctionComponent = ({ + user, + viewer, +}) => { const system = ( = ({ user }) => { return ( + {/* TODO: Localize */} + {viewer.role === GQLUSER_ROLE.ADMIN && ( + + )} @@ -294,6 +305,12 @@ const UserDrawerAccountHistory: FunctionComponent = ({ user }) => { }; const enhanced = withFragmentContainer({ + viewer: graphql` + fragment UserDrawerAccountHistory_viewer on User { + id + role + } + `, user: graphql` fragment UserDrawerAccountHistory_user on User { status { 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 ( + + ); }} /> ); From a147ad6c229dd12e0dc24f07e068b87b4431bcb9 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 8 Jan 2024 12:41:52 -0500 Subject: [PATCH 02/28] add popover for user delect account button --- .../DeleteAccountPopoverContainer.css | 90 ++++++++++ .../DeleteAccountPopoverContainer.tsx | 161 ++++++++++++++++++ .../UserDrawerAccountHistory.tsx | 6 +- 3 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css create mode 100644 client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx 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..2251c4959a --- /dev/null +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css @@ -0,0 +1,90 @@ +.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); +} + +.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); +} + +.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/DeleteAccountPopoverContainer.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx new file mode 100644 index 0000000000..13aa6346dd --- /dev/null +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx @@ -0,0 +1,161 @@ +import { Localized } from "@fluent/react/compat"; +import React, { FunctionComponent } from "react"; +import { graphql } from "react-relay"; + +import withFragmentContainer from "coral-framework/lib/relay/withFragmentContainer"; +import { AlertTriangleIcon, SvgIcon } from "coral-ui/components/icons"; +import { + Box, + Button, + CallOut, + ClickOutside, + Flex, + Popover, +} from "coral-ui/components/v2"; + +import { DeleteAccountPopoverContainer_user as UserData } from "coral-admin/__generated__/DeleteAccountPopoverContainer_user.graphql"; + +import styles from "./DeleteAccountPopoverContainer.css"; + +interface Props { + user: UserData; +} + +const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { + return ( + + ( + + + <> + +
Delete account
+
+ {/* TODO: Add styles here */} + +
Username
+
+
{user.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 entirely removes all records of this user + + + +
+ This will go into effect in 24 hours. +
+
+ +
+ Type in "delete" to confirm +
+
+ + {/* {banError && ( +
+ + {banError} +
+ )} */} + {/* */} + {/* )} */} + + + + + + + + + +
+
+ )} + > + {({ toggleVisibility, visible, ref }) => ( + + + + )} +
+
+ ); +}; + +const enhanced = withFragmentContainer({ + user: graphql` + fragment DeleteAccountPopoverContainer_user on User { + id + username + } + `, +})(DeleteAccountPopoverContainer); + +export default enhanced; diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx index 98d20649d8..bc0fd4a974 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx @@ -7,7 +7,6 @@ import { withFragmentContainer } from "coral-framework/lib/relay"; import { GQLUSER_ROLE } from "coral-framework/schema"; import { CoralMarkIcon, SvgIcon } from "coral-ui/components/icons"; import { - Button, CallOut, HorizontalGutter, Table, @@ -24,6 +23,7 @@ import AccountHistoryAction, { HistoryActionProps, } from "./AccountHistoryAction"; import { BanActionProps } from "./BanAction"; +import DeleteAccountPopoverContainer from "./DeleteAccountPopoverContainer"; import styles from "./UserDrawerAccountHistory.css"; @@ -270,9 +270,8 @@ const UserDrawerAccountHistory: FunctionComponent = ({ return ( - {/* TODO: Localize */} {viewer.role === GQLUSER_ROLE.ADMIN && ( - + )}
@@ -313,6 +312,7 @@ const enhanced = withFragmentContainer({ `, user: graphql` fragment UserDrawerAccountHistory_user on User { + ...DeleteAccountPopoverContainer_user status { username { history { From d267016e47d0960d36da315b189a4fe059b9e174 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 8 Jan 2024 14:06:01 -0500 Subject: [PATCH 03/28] add delete input check; also add scheduleAccountDeletion mutation --- .../DeleteAccountPopoverContainer.tsx | 46 ++++++++++-- .../ScheduleAccountDeletionMutation.tsx | 72 +++++++++++++++++++ .../src/core/server/graph/mutators/Users.ts | 13 ++++ .../core/server/graph/resolvers/Mutation.ts | 4 ++ .../core/server/graph/schema/schema.graphql | 37 +++++++++- .../src/core/server/services/users/users.ts | 50 +++++++++++++ 6 files changed, 215 insertions(+), 7 deletions(-) create mode 100644 client/src/core/client/admin/components/UserHistoryDrawer/ScheduleAccountDeletionMutation.tsx diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx index 13aa6346dd..1e2d307d0d 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx @@ -1,8 +1,13 @@ import { Localized } from "@fluent/react/compat"; -import React, { FunctionComponent } from "react"; +import React, { + FunctionComponent, + useCallback, + useMemo, + useState, +} from "react"; import { graphql } from "react-relay"; -import withFragmentContainer from "coral-framework/lib/relay/withFragmentContainer"; +import { useMutation, withFragmentContainer } from "coral-framework/lib/relay"; import { AlertTriangleIcon, SvgIcon } from "coral-ui/components/icons"; import { Box, @@ -15,6 +20,8 @@ import { import { DeleteAccountPopoverContainer_user as UserData } from "coral-admin/__generated__/DeleteAccountPopoverContainer_user.graphql"; +import ScheduleAccountDeletionMutation from "./ScheduleAccountDeletionMutation"; + import styles from "./DeleteAccountPopoverContainer.css"; interface Props { @@ -22,6 +29,33 @@ interface Props { } const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { + const scheduleAccountDeletion = useMutation(ScheduleAccountDeletionMutation); + + const onRequestDeletion = useCallback(async () => { + await scheduleAccountDeletion({ userID: user.id }); + // TODO: Handle error message + }, [user.id, scheduleAccountDeletion]); + + 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 ( = ({ user }) => { className={styles.confirmationInput} type="text" placeholder="" - // onChange={onSpamBanConfirmationTextInputChange} + onChange={onDeleteAccountConfirmationTextInputChange} /> {/* {banError && (
@@ -115,18 +149,18 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { variant="outlined" size="regular" color="mono" - onClick={() => {}} + onClick={toggleVisibility} > Cancel 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..cc5380cd08 --- /dev/null +++ b/client/src/core/client/admin/components/UserHistoryDrawer/ScheduleAccountDeletionMutation.tsx @@ -0,0 +1,72 @@ +import { graphql } from "react-relay"; +import { Environment } from "relay-runtime"; + +import { SCHEDULED_DELETION_WINDOW_DURATION } from "coral-common/common/lib/constants"; +import { getViewer } from "coral-framework/helpers"; +import { + commitMutationPromiseNormalized, + createMutation, + MutationInput, +} from "coral-framework/lib/relay"; +// import { ScheduleAccountDeletionEvent } from "coral-stream/events"; + +import { ScheduleAccountDeletionMutation as MutationTypes } from "coral-admin/__generated__/ScheduleAccountDeletionMutation.graphql"; + +let clientMutationId = 0; + +const ScheduleAccountDeletionMutation = createMutation( + "scheduleAccountDeletion", + async ( + environment: Environment, + input: MutationInput, + { eventEmitter } + ) => { + // const requestAccountDeletionEvent = + // ScheduleAccountDeletionEvent.begin(eventEmitter); + // try { + const result = await commitMutationPromiseNormalized( + environment, + { + mutation: graphql` + mutation ScheduleAccountDeletionMutation( + $input: ScheduleAccountDeletionInput! + ) { + scheduleAccountDeletion(input: $input) { + user { + scheduledDeletionDate + } + clientMutationId + } + } + `, + variables: { + input: { + userID: input.userID, + clientMutationId: (clientMutationId++).toString(), + }, + }, + optimisticUpdater: (store) => { + const viewer = getViewer(environment)!; + const deletionDate = new Date( + Date.now() + SCHEDULED_DELETION_WINDOW_DURATION * 1000 + ).toISOString(); + const viewerProxy = store.get(viewer.id); + if (viewerProxy) { + viewerProxy.setValue(deletionDate, "scheduledDeletionDate"); + } + }, + } + ); + // requestAccountDeletionEvent.success(); + return result; + // } catch (error) { + // requestAccountDeletionEvent.error({ + // message: error.message, + // code: error.code, + // }); + // throw error; + // } + } +); + +export default ScheduleAccountDeletionMutation; diff --git a/server/src/core/server/graph/mutators/Users.ts b/server/src/core/server/graph/mutators/Users.ts index 73b3d65428..db13dc1416 100644 --- a/server/src/core/server/graph/mutators/Users.ts +++ b/server/src/core/server/graph/mutators/Users.ts @@ -25,6 +25,7 @@ import { requestAccountDeletion, requestCommentsDownload, requestUserCommentsDownload, + scheduleAccountDeletion, sendModMessage, setEmail, setPassword, @@ -72,6 +73,7 @@ import { GQLRequestAccountDeletionInput, GQLRequestCommentsDownloadInput, GQLRequestUserCommentsDownloadInput, + GQLScheduleAccountDeletionInput, GQLSendModMessageInput, GQLSetEmailInput, GQLSetPasswordInput, @@ -174,6 +176,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> => { diff --git a/server/src/core/server/graph/resolvers/Mutation.ts b/server/src/core/server/graph/resolvers/Mutation.ts index 26e23ffb2d..1681a5022a 100644 --- a/server/src/core/server/graph/resolvers/Mutation.ts +++ b/server/src/core/server/graph/resolvers/Mutation.ts @@ -307,6 +307,10 @@ 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, + }), cancelAccountDeletion: async (source, { input }, ctx) => ({ user: await ctx.mutators.Users.cancelAccountDeletion(input), clientMutationId: input.clientMutationId, diff --git a/server/src/core/server/graph/schema/schema.graphql b/server/src/core/server/graph/schema/schema.graphql index 3345039c46..8ebf37f39a 100644 --- a/server/src/core/server/graph/schema/schema.graphql +++ b/server/src/core/server/graph/schema/schema.graphql @@ -6464,7 +6464,6 @@ input SettingsInput { """ flairBadges: FlairBadgeConfigurationInput - """ dsa specifies the configuration for DSA European Union moderation and reporting features. @@ -8252,6 +8251,34 @@ 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! +} + ################## # deleteUserAccount ################## @@ -10221,6 +10248,14 @@ 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]) + """ deleteUserAccount will delete the target user now. """ diff --git a/server/src/core/server/services/users/users.ts b/server/src/core/server/services/users/users.ts index 5c6ce5376c..c79a2764bd 100644 --- a/server/src/core/server/services/users/users.ts +++ b/server/src/core/server/services/users/users.ts @@ -599,6 +599,56 @@ export async function requestAccountDeletion( return updatedUser; } +export async function scheduleAccountDeletion( + mongo: MongoContext, + mailer: MailerQueue, + tenant: Tenant, + user: User, + userID: string, + now: Date +) { + // if (!user.email) { + // throw new EmailNotSetError(); + // } + + // const passwordVerified = await verifyUserPassword(user, password, user.email); + // if (!passwordVerified) { + // // We throw a PasswordIncorrect error here instead of an + // // InvalidCredentialsError because the current user is already signed in. + // throw new PasswordIncorrect(); + // } + + const deletionDate = DateTime.fromJSDate(now).plus({ + seconds: SCHEDULED_DELETION_WINDOW_DURATION, + }); + + const updatedUser = await scheduleDeletionDate( + mongo, + tenant.id, + userID, + deletionDate.toJSDate() + ); + + // const formattedDate = formatDate(deletionDate.toJSDate(), tenant.locale); + + // await mailer.add({ + // tenantID: tenant.id, + // message: { + // to: user.email, + // }, + // template: { + // name: "account-notification/delete-request-confirmation", + // context: { + // requestDate: formattedDate, + // organizationName: tenant.organization.name, + // organizationURL: tenant.organization.url, + // }, + // }, + // }); + + return updatedUser; +} + export async function cancelAccountDeletion( mongo: MongoContext, mailer: MailerQueue, From f5300ac14773594ab90b23964602362cf2adfdb4 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 8 Jan 2024 15:12:57 -0500 Subject: [PATCH 04/28] add cancel scheduled account deletion --- ...CancelScheduledAccountDeletionMutation.tsx | 66 +++++++++++++++++++ .../DeleteAccountPopoverContainer.css | 9 +++ .../DeleteAccountPopoverContainer.tsx | 49 ++++++++++++++ .../ScheduleAccountDeletionMutation.tsx | 8 +-- .../src/core/server/graph/mutators/Users.ts | 11 ++++ .../core/server/graph/resolvers/Mutation.ts | 4 ++ .../core/server/graph/schema/schema.graphql | 36 ++++++++++ .../src/core/server/services/users/users.ts | 29 ++++++++ 8 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 client/src/core/client/admin/components/UserHistoryDrawer/CancelScheduledAccountDeletionMutation.tsx 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..7061ea10a1 --- /dev/null +++ b/client/src/core/client/admin/components/UserHistoryDrawer/CancelScheduledAccountDeletionMutation.tsx @@ -0,0 +1,66 @@ +import { graphql } from "react-relay"; +import { Environment } from "relay-runtime"; + +import { + commitMutationPromiseNormalized, + createMutation, + MutationInput, +} from "coral-framework/lib/relay"; +// import { CancelScheduledAccountDeletionEvent } from "coral-stream/events"; + +import { CancelScheduledAccountDeletionMutation as MutationTypes } from "coral-admin/__generated__/CancelScheduledAccountDeletionMutation.graphql"; + +let clientMutationId = 0; + +const CancelScheduledAccountDeletionMutation = createMutation( + "cancelScheduleAccountDeletion", + async ( + environment: Environment, + input: MutationInput, + { eventEmitter } + ) => { + // const requestAccountDeletionEvent = + // CancelScheduledAccountDeletionEvent.begin(eventEmitter); + // try { + const result = await commitMutationPromiseNormalized( + environment, + { + mutation: graphql` + mutation CancelScheduledAccountDeletionMutation( + $input: CancelScheduledAccountDeletionInput! + ) { + cancelScheduledAccountDeletion(input: $input) { + user { + scheduledDeletionDate + } + clientMutationId + } + } + `, + variables: { + input: { + userID: input.userID, + clientMutationId: (clientMutationId++).toString(), + }, + }, + optimisticUpdater: (store) => { + const userProxy = store.get(input.userID); + if (userProxy) { + userProxy.setValue(null, "scheduledDeletionDate"); + } + }, + } + ); + // requestAccountDeletionEvent.success(); + return result; + // } catch (error) { + // requestAccountDeletionEvent.error({ + // message: error.message, + // code: error.code, + // }); + // throw error; + // } + } +); + +export default CancelScheduledAccountDeletionMutation; diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css index 2251c4959a..022d1f44ea 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css @@ -88,3 +88,12 @@ 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); +} + +.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 index 1e2d307d0d..17659cc559 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx @@ -7,6 +7,7 @@ import React, { } from "react"; import { graphql } from "react-relay"; +import { useDateTimeFormatter } from "coral-framework/hooks"; import { useMutation, withFragmentContainer } from "coral-framework/lib/relay"; import { AlertTriangleIcon, SvgIcon } from "coral-ui/components/icons"; import { @@ -23,6 +24,7 @@ import { DeleteAccountPopoverContainer_user as UserData } from "coral-admin/__ge import ScheduleAccountDeletionMutation from "./ScheduleAccountDeletionMutation"; import styles from "./DeleteAccountPopoverContainer.css"; +import CancelScheduledAccountDeletionMutation from "./CancelScheduledAccountDeletionMutation"; interface Props { user: UserData; @@ -30,12 +32,28 @@ interface Props { const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { const scheduleAccountDeletion = useMutation(ScheduleAccountDeletionMutation); + const cancelScheduledAccountDeletion = useMutation( + CancelScheduledAccountDeletionMutation + ); + + const formatter = useDateTimeFormatter({ + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }); const onRequestDeletion = useCallback(async () => { await scheduleAccountDeletion({ userID: user.id }); // TODO: Handle error message }, [user.id, scheduleAccountDeletion]); + const onCancelScheduledDeletion = useCallback(async () => { + await cancelScheduledAccountDeletion({ userID: user.id }); + }, [user.id, cancelScheduledAccountDeletion]); + const deleteAccountConfirmationText = "delete"; const [ deleteAccountConfirmationTextInput, @@ -56,6 +74,36 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { ); }, [deleteAccountConfirmationText, deleteAccountConfirmationTextInput]); + const deletionDate = useMemo( + () => + user.scheduledDeletionDate ? formatter(user.scheduledDeletionDate) : null, + [user, formatter] + ); + + if (deletionDate) { + return ( + + +
+ User deletion activated +
+
+ +
This will occur at {deletionDate}.
+
+ + + +
+ ); + } + return ( ({ fragment DeleteAccountPopoverContainer_user on User { id username + scheduledDeletionDate } `, })(DeleteAccountPopoverContainer); diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/ScheduleAccountDeletionMutation.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/ScheduleAccountDeletionMutation.tsx index cc5380cd08..1cd737de48 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/ScheduleAccountDeletionMutation.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/ScheduleAccountDeletionMutation.tsx @@ -2,7 +2,6 @@ import { graphql } from "react-relay"; import { Environment } from "relay-runtime"; import { SCHEDULED_DELETION_WINDOW_DURATION } from "coral-common/common/lib/constants"; -import { getViewer } from "coral-framework/helpers"; import { commitMutationPromiseNormalized, createMutation, @@ -46,13 +45,12 @@ const ScheduleAccountDeletionMutation = createMutation( }, }, optimisticUpdater: (store) => { - const viewer = getViewer(environment)!; const deletionDate = new Date( Date.now() + SCHEDULED_DELETION_WINDOW_DURATION * 1000 ).toISOString(); - const viewerProxy = store.get(viewer.id); - if (viewerProxy) { - viewerProxy.setValue(deletionDate, "scheduledDeletionDate"); + const userProxy = store.get(input.userID); + if (userProxy) { + userProxy.setValue(deletionDate, "scheduledDeletionDate"); } }, } diff --git a/server/src/core/server/graph/mutators/Users.ts b/server/src/core/server/graph/mutators/Users.ts index db13dc1416..bc4f4868b3 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, @@ -53,6 +54,7 @@ import { deleteUser } from "coral-server/services/users/delete"; import { GQLBanUserInput, GQLCancelAccountDeletionInput, + GQLCancelScheduledAccountDeletionInput, GQLCreateModeratorNoteInput, GQLCreateTokenInput, GQLDeactivateTokenInput, @@ -205,6 +207,15 @@ export const Users = (ctx: GraphContext) => ({ ctx.tenant.dsa?.enabled ); }, + cancelScheduledAccountDeletion: async ( + input: GQLCancelScheduledAccountDeletionInput + ): Promise | null> => + cancelScheduledAccountDeletion( + ctx.mongo, + ctx.mailerQueue, + ctx.tenant, + 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 1681a5022a..e5ac47184d 100644 --- a/server/src/core/server/graph/resolvers/Mutation.ts +++ b/server/src/core/server/graph/resolvers/Mutation.ts @@ -311,6 +311,10 @@ export const Mutation: Required> = { 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/schema/schema.graphql b/server/src/core/server/graph/schema/schema.graphql index 8ebf37f39a..487ed0924e 100644 --- a/server/src/core/server/graph/schema/schema.graphql +++ b/server/src/core/server/graph/schema/schema.graphql @@ -8279,6 +8279,34 @@ type ScheduleAccountDeletionPayload { 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 ################## @@ -10256,6 +10284,14 @@ type Mutation { 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/services/users/users.ts b/server/src/core/server/services/users/users.ts index c79a2764bd..e563004b27 100644 --- a/server/src/core/server/services/users/users.ts +++ b/server/src/core/server/services/users/users.ts @@ -649,6 +649,35 @@ export async function scheduleAccountDeletion( return updatedUser; } +export async function cancelScheduledAccountDeletion( + mongo: MongoContext, + mailer: MailerQueue, + tenant: Tenant, + userID: string +) { + // if (!user.email) { + // throw new EmailNotSetError(); + // } + + const updatedUser = await clearDeletionDate(mongo, tenant.id, userID); + + // await mailer.add({ + // tenantID: tenant.id, + // message: { + // to: user.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, From e19ab20c264e23e98862210396f9b9d8eca06296 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 8 Jan 2024 15:47:12 -0500 Subject: [PATCH 05/28] fix import order --- .../UserHistoryDrawer/DeleteAccountPopoverContainer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx index 17659cc559..68f19ff8eb 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx @@ -21,10 +21,10 @@ import { import { DeleteAccountPopoverContainer_user as UserData } from "coral-admin/__generated__/DeleteAccountPopoverContainer_user.graphql"; +import CancelScheduledAccountDeletionMutation from "./CancelScheduledAccountDeletionMutation"; import ScheduleAccountDeletionMutation from "./ScheduleAccountDeletionMutation"; import styles from "./DeleteAccountPopoverContainer.css"; -import CancelScheduledAccountDeletionMutation from "./CancelScheduledAccountDeletionMutation"; interface Props { user: UserData; From 92571ab38c918941cddb35295891c759642798db Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Tue, 9 Jan 2024 11:56:05 -0500 Subject: [PATCH 06/28] add user deletion history --- .../AccountHistoryAction.tsx | 11 +++- .../ScheduleAccountDeletionMutation.tsx | 1 + .../UserHistoryDrawer/UserDeletionAction.tsx | 32 ++++++++++ .../UserDrawerAccountHistory.tsx | 18 ++++++ .../src/core/client/test/helpers/fixture.ts | 3 + .../src/core/server/graph/mutators/Users.ts | 4 +- .../graph/resolvers/UserDeletionHistory.ts | 17 +++++ .../graph/resolvers/UserDeletionStatus.ts | 14 +++++ .../core/server/graph/resolvers/UserStatus.ts | 5 ++ .../src/core/server/graph/resolvers/index.ts | 4 ++ .../core/server/graph/schema/schema.graphql | 34 ++++++++++ server/src/core/server/models/user/user.ts | 62 ++++++++++++++++++- .../src/core/server/services/users/delete.ts | 14 ++++- .../src/core/server/services/users/users.ts | 22 +++++-- server/src/core/server/test/fixtures.ts | 3 + 15 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 client/src/core/client/admin/components/UserHistoryDrawer/UserDeletionAction.tsx create mode 100644 server/src/core/server/graph/resolvers/UserDeletionHistory.ts create mode 100644 server/src/core/server/graph/resolvers/UserDeletionStatus.ts 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/ScheduleAccountDeletionMutation.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/ScheduleAccountDeletionMutation.tsx index 1cd737de48..aa16969394 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/ScheduleAccountDeletionMutation.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/ScheduleAccountDeletionMutation.tsx @@ -45,6 +45,7 @@ const ScheduleAccountDeletionMutation = createMutation( }, }, optimisticUpdater: (store) => { + // TODO: Also add to the user's deletion history? const deletionDate = new Date( Date.now() + SCHEDULED_DELETION_WINDOW_DURATION * 1000 ).toISOString(); 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..95d8d62238 --- /dev/null +++ b/client/src/core/client/admin/components/UserHistoryDrawer/UserDeletionAction.tsx @@ -0,0 +1,32 @@ +import { Localized } from "@fluent/react/compat"; +import React, { FunctionComponent } from "react"; + +export interface UserDeletionActionProps { + action: "CANCELED" | "REQUESTED" | "COMPLETED" | "%future added value"; +} + +const UserDeletionAction: FunctionComponent = ({ + action, +}) => { + if (action === "REQUESTED") { + return ( + + User scheduled for deletion + + ); + } else if (action === "CANCELED") { + return ( + + User deletion request canceled + + ); + } else if (action === "COMPLETED") { + return ( + + User deleted + + ); + } + return null; +}; +export default UserDeletionAction; diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx index bc0fd4a974..a5183c0a67 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx @@ -224,6 +224,15 @@ const UserDrawerAccountHistory: FunctionComponent = ({ }); }); + 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 }, + }); + }); + // Sort the history so that it's in the right order. const dateSortedHistory = history.sort( (a, b) => b.date.getTime() - a.date.getTime() @@ -323,6 +332,15 @@ const enhanced = withFragmentContainer({ } } } + deletion { + history { + updateType + createdAt + createdBy { + username + } + } + } warning { history { active 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/server/src/core/server/graph/mutators/Users.ts b/server/src/core/server/graph/mutators/Users.ts index bc4f4868b3..4c19502add 100644 --- a/server/src/core/server/graph/mutators/Users.ts +++ b/server/src/core/server/graph/mutators/Users.ts @@ -204,7 +204,8 @@ 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 ( @@ -214,6 +215,7 @@ export const Users = (ctx: GraphContext) => ({ ctx.mongo, ctx.mailerQueue, ctx.tenant, + ctx.user!, input.userID ), cancelAccountDeletion: async ( 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 487ed0924e..12d5f99a8f 100644 --- a/server/src/core/server/graph/schema/schema.graphql +++ b/server/src/core/server/graph/schema/schema.graphql @@ -2693,6 +2693,30 @@ type UsernameStatus { history: [UsernameHistory!]! } +enum UserDeletionUpdateType { + REQUESTED + CANCELED + COMPLETED +} + +type UserDeletionHistory { + 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. """ @@ -2717,6 +2741,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. """ diff --git a/server/src/core/server/models/user/user.ts b/server/src/core/server/models/user/user.ts index 398821c7e3..bd4fb9412a 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,17 @@ export interface UsernameStatus { history: UsernameHistory[]; } +export interface UserDeletionHistory { + id: string; + updateType: GQLUserDeletionUpdateType; + createdBy: string; + createdAt: string; +} + +export interface UserDeletionStatus { + history: UserDeletionHistory[]; +} + /** * PremodStatusHistory is the history of premod status changes * against a specific User. @@ -406,6 +419,8 @@ export interface UserStatus { * a history of moderation messages */ modMessage?: ModMessageStatus; + + deletion: UserDeletionStatus; } /** @@ -670,6 +685,9 @@ export async function findOrCreateUserInput( premod: { active: false, history: [] }, warning: { active: false, history: [] }, modMessage: { active: false, history: [] }, + deletion: { + history: [], + }, }, notifications: { onReply: false, @@ -1154,9 +1172,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, @@ -1166,6 +1193,9 @@ export async function scheduleDeletionDate( $set: { scheduledDeletionDate: deletionDate, }, + $push: { + "status.deletion.history": scheduleDeletionHistory, + }, }, { returnOriginal: false, @@ -1181,8 +1211,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, @@ -1192,6 +1230,9 @@ export async function clearDeletionDate( $unset: { scheduledDeletionDate: "", }, + $push: { + "status.deletion.history": cancelDeletionHistory, + }, }, { // We want to return edited user so that @@ -2654,6 +2695,12 @@ export type ConsolidatedBanStatus = Omit & export type ConsolidatedUsernameStatus = Omit & Pick; +export type ConsolidatedUserDeletionStatus = Omit< + GQLUserDeletionStatus, + "history" +> & + Pick; + export type ConsolidatedPremodStatus = Omit & Pick; @@ -2672,6 +2719,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/services/users/delete.ts b/server/src/core/server/services/users/delete.ts index 0284eeaf2c..622d9e37a3 100644 --- a/server/src/core/server/services/users/delete.ts +++ b/server/src/core/server/services/users/delete.ts @@ -15,6 +15,7 @@ import { GQLDSAReportStatus, GQLREJECTION_REASON_CODE, GQLRejectionReason, + GQLUserDeletionUpdateType, } from "coral-server/graph/schema/__generated__/types"; import { moderate } from "../comments/moderation"; @@ -365,7 +366,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) { @@ -412,6 +414,13 @@ export async function deleteUser( ); } + const deletionHistory = { + id: uuid(), + updateType: GQLUserDeletionUpdateType.COMPLETED, + createdBy: requestingUser, + createdAt: now, + }; + // Mark the user as deleted. const result = await mongo.users().findOneAndUpdate( { tenantID, id: userID }, @@ -423,6 +432,9 @@ export async function deleteUser( profiles: "", email: "", }, + $push: { + "status.deletion.history": deletionHistory, + }, }, { // False to return the updated document instead of the original diff --git a/server/src/core/server/services/users/users.ts b/server/src/core/server/services/users/users.ts index e563004b27..fefa437236 100644 --- a/server/src/core/server/services/users/users.ts +++ b/server/src/core/server/services/users/users.ts @@ -576,6 +576,7 @@ export async function requestAccountDeletion( mongo, tenant.id, user.id, + user.id, deletionDate.toJSDate() ); @@ -603,7 +604,7 @@ export async function scheduleAccountDeletion( mongo: MongoContext, mailer: MailerQueue, tenant: Tenant, - user: User, + requestingUser: User, userID: string, now: Date ) { @@ -625,8 +626,10 @@ export async function scheduleAccountDeletion( const updatedUser = await scheduleDeletionDate( mongo, tenant.id, + requestingUser.id, userID, - deletionDate.toJSDate() + deletionDate.toJSDate(), + now ); // const formattedDate = formatDate(deletionDate.toJSDate(), tenant.locale); @@ -653,13 +656,19 @@ export async function cancelScheduledAccountDeletion( mongo: MongoContext, mailer: MailerQueue, tenant: Tenant, + user: User, userID: string ) { // if (!user.email) { // throw new EmailNotSetError(); // } - const updatedUser = await clearDeletionDate(mongo, tenant.id, userID); + const updatedUser = await clearDeletionDate( + mongo, + tenant.id, + userID, + user.id + ); // await mailer.add({ // tenantID: tenant.id, @@ -688,7 +697,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 b27fa72372..6f5a592379 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: [ { From a963049292ff778c8b7f048a782b2e2e7f619765 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Tue, 9 Jan 2024 15:20:16 -0500 Subject: [PATCH 07/28] add localization; styles; handle errors; improve mutation results --- ...CancelScheduledAccountDeletionMutation.tsx | 35 ++---- .../DeleteAccountPopoverContainer.css | 10 ++ .../DeleteAccountPopoverContainer.tsx | 103 +++++++++++------- .../ScheduleAccountDeletionMutation.tsx | 40 ++----- .../UserHistoryDrawer/UserDeletionAction.tsx | 6 +- locales/en-US/admin.ftl | 20 ++++ .../src/core/server/services/users/users.ts | 15 --- 7 files changed, 123 insertions(+), 106 deletions(-) diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/CancelScheduledAccountDeletionMutation.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/CancelScheduledAccountDeletionMutation.tsx index 7061ea10a1..17ea643321 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/CancelScheduledAccountDeletionMutation.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/CancelScheduledAccountDeletionMutation.tsx @@ -6,7 +6,6 @@ import { createMutation, MutationInput, } from "coral-framework/lib/relay"; -// import { CancelScheduledAccountDeletionEvent } from "coral-stream/events"; import { CancelScheduledAccountDeletionMutation as MutationTypes } from "coral-admin/__generated__/CancelScheduledAccountDeletionMutation.graphql"; @@ -14,14 +13,7 @@ let clientMutationId = 0; const CancelScheduledAccountDeletionMutation = createMutation( "cancelScheduleAccountDeletion", - async ( - environment: Environment, - input: MutationInput, - { eventEmitter } - ) => { - // const requestAccountDeletionEvent = - // CancelScheduledAccountDeletionEvent.begin(eventEmitter); - // try { + async (environment: Environment, input: MutationInput) => { const result = await commitMutationPromiseNormalized( environment, { @@ -32,6 +24,17 @@ const CancelScheduledAccountDeletionMutation = createMutation( cancelScheduledAccountDeletion(input: $input) { user { scheduledDeletionDate + status { + deletion { + history { + updateType + createdBy { + username + } + createdAt + } + } + } } clientMutationId } @@ -43,23 +46,9 @@ const CancelScheduledAccountDeletionMutation = createMutation( clientMutationId: (clientMutationId++).toString(), }, }, - optimisticUpdater: (store) => { - const userProxy = store.get(input.userID); - if (userProxy) { - userProxy.setValue(null, "scheduledDeletionDate"); - } - }, } ); - // requestAccountDeletionEvent.success(); return result; - // } catch (error) { - // requestAccountDeletionEvent.error({ - // message: error.message, - // code: error.code, - // }); - // throw error; - // } } ); diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css index 022d1f44ea..1cf41263f3 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css @@ -23,6 +23,10 @@ margin-top: var(--spacing-3); } +.username { + font-size: var(--font-size-2); +} + .orderedList { margin: 0; padding-left: var(--spacing-4); @@ -42,6 +46,7 @@ .moreInfo { font-size: var(--font-size-2); + margin-top: var(--spacing-2); } .icon { @@ -94,6 +99,11 @@ 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 index 68f19ff8eb..6714b00857 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx @@ -9,7 +9,11 @@ import { graphql } from "react-relay"; import { useDateTimeFormatter } from "coral-framework/hooks"; import { useMutation, withFragmentContainer } from "coral-framework/lib/relay"; -import { AlertTriangleIcon, SvgIcon } from "coral-ui/components/icons"; +import { + AlertCircleIcon, + AlertTriangleIcon, + SvgIcon, +} from "coral-ui/components/icons"; import { Box, Button, @@ -35,6 +39,12 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { const cancelScheduledAccountDeletion = useMutation( CancelScheduledAccountDeletionMutation ); + const [requestDeletionError, setRequestDeletionError] = useState< + string | null + >(null); + const [cancelDeletionError, setCancelDeletionError] = useState( + null + ); const formatter = useDateTimeFormatter({ year: "numeric", @@ -46,13 +56,24 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { }); const onRequestDeletion = useCallback(async () => { - await scheduleAccountDeletion({ userID: user.id }); - // TODO: Handle error message - }, [user.id, scheduleAccountDeletion]); + try { + await scheduleAccountDeletion({ userID: user.id }); + } catch (e) { + if (e.message) { + setRequestDeletionError(e.message); + } + } + }, [user.id, scheduleAccountDeletion, setRequestDeletionError]); const onCancelScheduledDeletion = useCallback(async () => { - await cancelScheduledAccountDeletion({ userID: user.id }); - }, [user.id, cancelScheduledAccountDeletion]); + try { + await cancelScheduledAccountDeletion({ userID: user.id }); + } catch (e) { + if (e.message) { + setCancelDeletionError(e.message); + } + } + }, [user.id, cancelScheduledAccountDeletion, setCancelDeletionError]); const deleteAccountConfirmationText = "delete"; const [ @@ -83,15 +104,20 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { if (deletionDate) { return ( - +
User deletion activated
- -
This will occur at {deletionDate}.
+ +
+ This will occur at {deletionDate}. +
- + + {cancelDeletionError && ( +
+ {" "} + + {cancelDeletionError} +
+ )}
); } @@ -108,33 +141,31 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { ( + placement="right-start" + description="A popover menu to delete a user's account" + body={({ toggleVisibility }) => ( <> - +
Delete account
- {/* TODO: Add styles here */} - +
Username
-
{user.username}
- +
{user.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 @@ -156,21 +187,21 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { className={styles.icon} Icon={AlertTriangleIcon} /> - + This entirely removes all records of this user - +
    This will go into effect in 24 hours.
    - Type in "delete" to confirm + Type in "{deleteAccountConfirmationText}" to confirm
    = ({ user }) => { placeholder="" onChange={onDeleteAccountConfirmationTextInputChange} /> - {/* {banError && ( -
    - - {banError} -
    - )} */} - {/* */} - {/* )} */} + {requestDeletionError && ( +
    + + {requestDeletionError} +
    + )} - + - + {cancelDeletionError && ( @@ -138,7 +138,10 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { } return ( - + = ({ user }) => {
  4. Remove all comments written by this user from the - database + database.
  5. @@ -170,7 +173,7 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { 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 + history, press "CANCEL" and use the Status dropdown below the username. @@ -249,7 +252,10 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { )} > {({ toggleVisibility, ref }) => ( - + diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx index a5183c0a67..f56070e233 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx @@ -1,8 +1,11 @@ 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"; @@ -66,6 +69,63 @@ const UserDrawerAccountHistory: FunctionComponent = ({ ); + + 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)) } + ), + COMPLETED: getMessage( + localeBundles, + "moderate-user-drawer-account-history-completed-at", + `Completed 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[] = []; @@ -230,6 +290,10 @@ const UserDrawerAccountHistory: FunctionComponent = ({ date: new Date(record.createdAt), takenBy: record.createdBy ? record.createdBy.username : system, action: { action: record.updateType }, + description: deletionDescriptionMapping( + record.updateType, + record.createdAt + ), }); }); @@ -243,7 +307,7 @@ const UserDrawerAccountHistory: FunctionComponent = ({ } return dateSortedHistory; - }, [system, user.status]); + }, [system, user.status, deletionDescriptionMapping]); const formatter = useDateTimeFormatter({ year: "numeric", month: "long", diff --git a/locales/en-US/admin.ftl b/locales/en-US/admin.ftl index ccc57626c5..75340087aa 100644 --- a/locales/en-US/admin.ftl +++ b/locales/en-US/admin.ftl @@ -1228,14 +1228,18 @@ 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-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. + retain their history, press "CANCEL" and use the Status dropdown below the username. moderate-user-drawer-deleteAccount-popover-callout = This entirely 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 @@ -1243,12 +1247,17 @@ 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 account deletion +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-user-deletion-completed = User deleted +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-completed-at = Completed 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 = Calculated over the last { framework-timeago-time } From 72a88e3e1c00ad9d2802b00b632f358464874d9c Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Wed, 10 Jan 2024 10:39:36 -0500 Subject: [PATCH 10/28] move DeleteAccountPopover into own component --- .../DeleteAccountPopover.css | 95 +++++++++++ .../DeleteAccountPopover.tsx | 160 ++++++++++++++++++ .../DeleteAccountPopoverContainer.css | 83 --------- .../DeleteAccountPopoverContainer.tsx | 156 +---------------- 4 files changed, 263 insertions(+), 231 deletions(-) create mode 100644 client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.css create mode 100644 client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx 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..68ee29a0a5 --- /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); + } + } + }, [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 entirely 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 index 1cf41263f3..783cb33ad2 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.css @@ -1,54 +1,3 @@ -.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); @@ -56,38 +5,6 @@ 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); diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx index 0f9c70f94d..06543c0c8c 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx @@ -9,24 +9,13 @@ import { graphql } from "react-relay"; import { useDateTimeFormatter } from "coral-framework/hooks"; import { useMutation, withFragmentContainer } from "coral-framework/lib/relay"; -import { - AlertCircleIcon, - AlertTriangleIcon, - SvgIcon, -} from "coral-ui/components/icons"; -import { - Box, - Button, - CallOut, - ClickOutside, - Flex, - Popover, -} from "coral-ui/components/v2"; +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 ScheduleAccountDeletionMutation from "./ScheduleAccountDeletionMutation"; +import DeleteAccountPopover from "./DeleteAccountPopover"; import styles from "./DeleteAccountPopoverContainer.css"; @@ -35,13 +24,9 @@ interface Props { } const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { - const scheduleAccountDeletion = useMutation(ScheduleAccountDeletionMutation); const cancelScheduledAccountDeletion = useMutation( CancelScheduledAccountDeletionMutation ); - const [requestDeletionError, setRequestDeletionError] = useState< - string | null - >(null); const [cancelDeletionError, setCancelDeletionError] = useState( null ); @@ -55,16 +40,6 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { second: "numeric", }); - const onRequestDeletion = useCallback(async () => { - try { - await scheduleAccountDeletion({ userID: user.id }); - } catch (e) { - if (e.message) { - setRequestDeletionError(e.message); - } - } - }, [user.id, scheduleAccountDeletion, setRequestDeletionError]); - const onCancelScheduledDeletion = useCallback(async () => { try { await cancelScheduledAccountDeletion({ userID: user.id }); @@ -75,26 +50,6 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { } }, [user.id, cancelScheduledAccountDeletion, setCancelDeletionError]); - 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]); - const deletionDate = useMemo( () => user.scheduledDeletionDate ? formatter(user.scheduledDeletionDate) : null, @@ -148,106 +103,11 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { description="A popover menu to delete a user's account" body={({ toggleVisibility }) => ( - - <> - -
    Delete account
    -
    - -
    Username
    -
    -
    {user.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 entirely removes all records of this user - - - -
    - This will go into effect in 24 hours. -
    -
    - -
    - Type in "{deleteAccountConfirmationText}" to confirm -
    -
    - - {requestDeletionError && ( -
    - - {requestDeletionError} -
    - )} - - - - - - - - - -
    +
    )} > From 11158603a135502ba699b11607653a4bba70afd0 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Wed, 10 Jan 2024 13:02:42 -0500 Subject: [PATCH 11/28] copy update --- .../admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx | 2 +- locales/en-US/admin.ftl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx index 68ee29a0a5..5fa4fe0d84 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx @@ -97,7 +97,7 @@ const DeleteAccountPopover: FunctionComponent = ({ - This entirely removes all records of this user + This removes all records of this user diff --git a/locales/en-US/admin.ftl b/locales/en-US/admin.ftl index 75340087aa..c91a74f650 100644 --- a/locales/en-US/admin.ftl +++ b/locales/en-US/admin.ftl @@ -1240,7 +1240,7 @@ moderate-user-drawer-deleteAccount-popover-description-list-removeComments = Rem 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 entirely removes all records of this user +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 From 22d36570cecc287702283a8e4b9c912a02c96076 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Wed, 10 Jan 2024 15:21:23 -0500 Subject: [PATCH 12/28] add initial user account history delete button test --- .../UserDrawerAccountHistory.css | 4 +++ .../UserDrawerAccountHistory.tsx | 17 +++++++++---- .../test/community/userHistoryDrawer.spec.tsx | 25 +++++++++++++++++++ client/src/core/client/admin/test/fixtures.ts | 6 +++++ 4 files changed, 47 insertions(+), 5 deletions(-) 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 f56070e233..bdf90ce6df 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx @@ -316,11 +316,18 @@ const UserDrawerAccountHistory: FunctionComponent = ({ 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 + + + ); } diff --git a/client/src/core/client/admin/test/community/userHistoryDrawer.spec.tsx b/client/src/core/client/admin/test/community/userHistoryDrawer.spec.tsx index 8df58c5cea..9cf49f1d84 100644 --- a/client/src/core/client/admin/test/community/userHistoryDrawer.spec.tsx +++ b/client/src/core/client/admin/test/community/userHistoryDrawer.spec.tsx @@ -77,6 +77,31 @@ it("user drawer is open and both user name and user id are visible", async () => ).toBeInTheDocument(); }); +it("user drawer is open and user can be deleted if admin", async () => { + await createTestRenderer(); + 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(); +}); + 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: [], + }, }, }); From 9cc4b93b52c9788f2a3fd18b65069174360442dc Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 11 Jan 2024 08:42:28 -0500 Subject: [PATCH 13/28] finish user account history deletion button test --- .../test/community/userHistoryDrawer.spec.tsx | 93 ++++++++++++++++++- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/client/src/core/client/admin/test/community/userHistoryDrawer.spec.tsx b/client/src/core/client/admin/test/community/userHistoryDrawer.spec.tsx index 9cf49f1d84..d50082ab9e 100644 --- a/client/src/core/client/admin/test/community/userHistoryDrawer.spec.tsx +++ b/client/src/core/client/admin/test/community/userHistoryDrawer.spec.tsx @@ -1,8 +1,18 @@ -import { act, screen, waitFor, within } from "@testing-library/react"; +import { + act, + fireEvent, + screen, + waitFor, + within, +} from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { pureMerge } from "coral-common/common/lib/utils"; -import { GQLFEATURE_FLAG, GQLResolver } from "coral-framework/schema"; +import { + GQLFEATURE_FLAG, + GQLResolver, + GQLUserDeletionUpdateType, +} from "coral-framework/schema"; import { createResolversStub, CreateTestRendererParams, @@ -77,8 +87,62 @@ it("user drawer is open and both user name and user id are visible", async () => ).toBeInTheDocument(); }); -it("user drawer is open and user can be deleted if admin", async () => { - await createTestRenderer(); +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 () => { @@ -100,6 +164,27 @@ it("user drawer is open and user can be deleted if admin", async () => { 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 () => { From c3f81b41a69bdbf3302526f6307d67d9e3426d11 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 11 Jan 2024 09:03:25 -0500 Subject: [PATCH 14/28] completed tracked by deletedAt and also never shown --- .../UserHistoryDrawer/UserDeletionAction.tsx | 8 +------- .../UserHistoryDrawer/UserDrawerAccountHistory.tsx | 6 ------ locales/en-US/admin.ftl | 2 -- server/src/core/server/graph/schema/schema.graphql | 3 +-- server/src/core/server/services/users/delete.ts | 11 ----------- 5 files changed, 2 insertions(+), 28 deletions(-) diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/UserDeletionAction.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/UserDeletionAction.tsx index 55a6cc8642..976c0c8bc5 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/UserDeletionAction.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/UserDeletionAction.tsx @@ -2,7 +2,7 @@ import { Localized } from "@fluent/react/compat"; import React, { FunctionComponent } from "react"; export interface UserDeletionActionProps { - action: "CANCELED" | "REQUESTED" | "COMPLETED" | "%future added value"; + action: "CANCELED" | "REQUESTED" | "%future added value"; } const UserDeletionAction: FunctionComponent = ({ @@ -20,12 +20,6 @@ const UserDeletionAction: FunctionComponent = ({ User deletion request canceled
    ); - } else if (action === "COMPLETED") { - return ( - - User deleted - - ); } return null; }; diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx index bdf90ce6df..2fd93557c4 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/UserDrawerAccountHistory.tsx @@ -108,12 +108,6 @@ const UserDrawerAccountHistory: FunctionComponent = ({ `Canceled at ${deletionFormatter(new Date(createdAt))}`, { createdAt: deletionFormatter(new Date(createdAt)) } ), - COMPLETED: getMessage( - localeBundles, - "moderate-user-drawer-account-history-completed-at", - `Completed at ${deletionFormatter(new Date(createdAt))}`, - { createdAt: deletionFormatter(new Date(createdAt)) } - ), "%future added value": getMessage( localeBundles, "moderate-user-drawer-account-history-updated-at", diff --git a/locales/en-US/admin.ftl b/locales/en-US/admin.ftl index c91a74f650..f4b91dba4f 100644 --- a/locales/en-US/admin.ftl +++ b/locales/en-US/admin.ftl @@ -1251,11 +1251,9 @@ moderate-user-drawer-deleteAccount-scheduled-cancelDeletion = Cancel user deleti moderate-user-drawer-user-scheduled-deletion = User scheduled for deletion moderate-user-drawer-user-deletion-canceled = User deletion request canceled -moderate-user-drawer-user-deletion-completed = User deleted 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-completed-at = Completed at { $createdAt } moderate-user-drawer-account-history-updated-at = Updated at { $createdAt } moderate-user-drawer-recent-history-title = Recent comment history diff --git a/server/src/core/server/graph/schema/schema.graphql b/server/src/core/server/graph/schema/schema.graphql index 9f2711f276..51fe0689db 100644 --- a/server/src/core/server/graph/schema/schema.graphql +++ b/server/src/core/server/graph/schema/schema.graphql @@ -2696,13 +2696,12 @@ type UsernameStatus { enum UserDeletionUpdateType { REQUESTED CANCELED - COMPLETED } type UserDeletionHistory { """ updateType is the type of deletion status update that was made. For example, - user deletion was requested, canceled, or completed. + user deletion was requested or canceled. """ updateType: UserDeletionUpdateType! diff --git a/server/src/core/server/services/users/delete.ts b/server/src/core/server/services/users/delete.ts index 622d9e37a3..822317ffa7 100644 --- a/server/src/core/server/services/users/delete.ts +++ b/server/src/core/server/services/users/delete.ts @@ -15,7 +15,6 @@ import { GQLDSAReportStatus, GQLREJECTION_REASON_CODE, GQLRejectionReason, - GQLUserDeletionUpdateType, } from "coral-server/graph/schema/__generated__/types"; import { moderate } from "../comments/moderation"; @@ -414,13 +413,6 @@ export async function deleteUser( ); } - const deletionHistory = { - id: uuid(), - updateType: GQLUserDeletionUpdateType.COMPLETED, - createdBy: requestingUser, - createdAt: now, - }; - // Mark the user as deleted. const result = await mongo.users().findOneAndUpdate( { tenantID, id: userID }, @@ -432,9 +424,6 @@ export async function deleteUser( profiles: "", email: "", }, - $push: { - "status.deletion.history": deletionHistory, - }, }, { // False to return the updated document instead of the original From 43bd2debf95a53ed40507006eef27227020b8975 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 11 Jan 2024 10:26:07 -0500 Subject: [PATCH 15/28] update notification emails and comments --- .../DeleteAccountPopoverContainer.tsx | 2 +- server/src/core/server/models/user/user.ts | 2 +- .../delete-request-cancel.html | 2 +- .../delete-request-confirmation.html | 4 +- .../src/core/server/services/users/users.ts | 66 ++++++++++--------- 5 files changed, 40 insertions(+), 36 deletions(-) diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx index 06543c0c8c..28b1f2c038 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx @@ -98,7 +98,7 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { attrs={{ description: true }} > ( diff --git a/server/src/core/server/models/user/user.ts b/server/src/core/server/models/user/user.ts index 71ad672008..1e867aa7d8 100644 --- a/server/src/core/server/models/user/user.ts +++ b/server/src/core/server/models/user/user.ts @@ -284,7 +284,7 @@ export interface UserDeletionHistory { /** * updateType is the kind of update to a user's deletion status that was made, - * whether it was requested, canceled, or completed + * whether it was requested or canceled. */ updateType: GQLUserDeletionUpdateType; 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/users.ts b/server/src/core/server/services/users/users.ts index 2182b985db..29fb0dca23 100644 --- a/server/src/core/server/services/users/users.ts +++ b/server/src/core/server/services/users/users.ts @@ -621,22 +621,24 @@ export async function scheduleAccountDeletion( now ); - // const formattedDate = formatDate(deletionDate.toJSDate(), tenant.locale); - - // await mailer.add({ - // tenantID: tenant.id, - // message: { - // to: user.email, - // }, - // template: { - // name: "account-notification/delete-request-confirmation", - // context: { - // requestDate: formattedDate, - // organizationName: tenant.organization.name, - // organizationURL: tenant.organization.url, - // }, - // }, - // }); + 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; } @@ -645,29 +647,31 @@ export async function cancelScheduledAccountDeletion( mongo: MongoContext, mailer: MailerQueue, tenant: Tenant, - user: User, + requestingUser: User, userID: string ) { const updatedUser = await clearDeletionDate( mongo, tenant.id, userID, - user.id + requestingUser.id ); - // await mailer.add({ - // tenantID: tenant.id, - // message: { - // to: user.email, - // }, - // template: { - // name: "account-notification/delete-request-cancel", - // context: { - // organizationName: tenant.organization.name, - // organizationURL: tenant.organization.url, - // }, - // }, - // }); + 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; } From ac7ea5fc47e30b0fb279f28b62264f16348e8588 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 11 Jan 2024 13:17:24 -0500 Subject: [PATCH 16/28] update tweets to X posts --- .../sections/General/MediaLinksConfig.tsx | 6 ++-- .../MediaConfirmation/MediaConfirmPrompt.tsx | 2 +- .../MediaConfirmationIcon.css | 4 --- .../MediaConfirmationIcon.tsx | 9 ++---- .../MediaSection/MediaSectionContainer.tsx | 4 +-- .../Preferences/MediaSettingsContainer.tsx | 2 +- .../icons/SocialMediaTwitterIcon.tsx | 17 ----------- .../ui/components/icons/XLogoTwitterIcon.tsx | 29 +++++++++++++++++++ .../core/client/ui/components/icons/index.ts | 2 +- locales/en-US/admin.ftl | 4 +-- locales/en-US/stream.ftl | 10 +++---- 11 files changed, 45 insertions(+), 44 deletions(-) delete mode 100644 client/src/core/client/stream/tabs/Comments/Comment/MediaConfirmation/MediaConfirmationIcon.css delete mode 100644 client/src/core/client/ui/components/icons/SocialMediaTwitterIcon.tsx create mode 100644 client/src/core/client/ui/components/icons/XLogoTwitterIcon.tsx 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 }) => { - + = ({ {media.type === "twitter" && (

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

    )} 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..1701253006 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" && ( 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/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/locales/en-US/admin.ftl b/locales/en-US/admin.ftl index 36dc9c55cb..464d72d1f5 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 diff --git a/locales/en-US/stream.ftl b/locales/en-US/stream.ftl index 3c65e33f98..184f4c769a 100644 --- a/locales/en-US/stream.ftl +++ b/locales/en-US/stream.ftl @@ -124,9 +124,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 @@ -463,8 +463,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 @@ -560,7 +560,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 = From a40d168c863bbc5bde38cc4286476bf87889fac3 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 11 Jan 2024 13:19:39 -0500 Subject: [PATCH 17/28] update in a few more places to post --- .../Comments/Comment/MediaConfirmation/MediaConfirmPrompt.tsx | 2 +- .../Comments/Comment/MediaSection/MediaSectionContainer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 eda843a2ad..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 @@ -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/MediaSection/MediaSectionContainer.tsx b/client/src/core/client/stream/tabs/Comments/Comment/MediaSection/MediaSectionContainer.tsx index 1701253006..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 @@ -137,7 +137,7 @@ const MediaSectionContainer: FunctionComponent = ({ /> {media.__typename === "TwitterMedia" && ( - Hide Tweet + Hide post )} {media.__typename === "GiphyMedia" && ( From 65dd132212478400db1c6c605de0d5bb50780341 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Fri, 12 Jan 2024 10:52:54 -0500 Subject: [PATCH 18/28] add new outlook accounts to protected email domains --- common/lib/constants.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/lib/constants.ts b/common/lib/constants.ts index 50f859c4e0..a018c74e82 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.-]+$"; From 1bde411b3749f9b5fd30df580fae885453ecc0ce Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 18 Jan 2024 09:52:05 -0500 Subject: [PATCH 19/28] add ability to use rootURL for local wp plugin amp dev --- server/src/core/server/app/router/client.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/core/server/app/router/client.ts b/server/src/core/server/app/router/client.ts index 7f3335d2ce..23e8ec9320 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,11 @@ const clientHandler = rootURL = `${req.protocol}://${req.coral.tenant?.domain}`; } + // this supports local wordpress plugin development + if (config.get("env") === "development" && req.query.rootURL) { + rootURL = req.query.rootURL; + } + const entrypoint = await entrypointLoader(); if (!entrypoint) { next(new Error("Entrypoint not available")); From 0947caf1e0485a16ce6e072fb0fb136024afc224 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 18 Jan 2024 10:23:42 -0500 Subject: [PATCH 20/28] just add port for dev --- server/src/core/server/app/router/client.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/core/server/app/router/client.ts b/server/src/core/server/app/router/client.ts index 23e8ec9320..704bf85393 100644 --- a/server/src/core/server/app/router/client.ts +++ b/server/src/core/server/app/router/client.ts @@ -175,8 +175,9 @@ const clientHandler = } // this supports local wordpress plugin development - if (config.get("env") === "development" && req.query.rootURL) { - rootURL = req.query.rootURL; + if (config.get("env") === "development") { + const port = config.get("port"); + rootURL = `${req.protocol}://${req.coral.tenant?.domain}:${port}`; } const entrypoint = await entrypointLoader(); From 055c9af7784c90ffe3e1153e9b92cde5ee40d9b9 Mon Sep 17 00:00:00 2001 From: nick-funk Date: Thu, 18 Jan 2024 11:07:14 -0700 Subject: [PATCH 21/28] create docker-compose + README for wordpress dev environment --- wordpress/README.md | 33 +++++++++++++++++++++++++++++++++ wordpress/docker-compose.yml | 21 +++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 wordpress/README.md create mode 100644 wordpress/docker-compose.yml diff --git a/wordpress/README.md b/wordpress/README.md new file mode 100644 index 0000000000..96ce91bbc4 --- /dev/null +++ b/wordpress/README.md @@ -0,0 +1,33 @@ +## 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 + ``` + +- Spin up Coral and add `http://localhost:8081` to a new or existing site so the new Wordpress URL is allowed. + +- Then 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. + +- 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 From fc7d0cd0534381931ec7c6a06ffc56e8855c565f Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Thu, 18 Jan 2024 13:17:29 -0500 Subject: [PATCH 22/28] update banned word rejection messaging --- .../stream/tabs/Comments/Comment/ReplyEditSubmitStatus.tsx | 3 ++- .../Stream/PostCommentForm/PostCommentRejectedMessage.tsx | 5 ++++- locales/en-US/stream.ftl | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) 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/locales/en-US/stream.ftl b/locales/en-US/stream.ftl index 9fda6fbfdd..493bd1a620 100644 --- a/locales/en-US/stream.ftl +++ b/locales/en-US/stream.ftl @@ -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 From aa09011d2b3a6871041e8e0dac6a6da44e46a913 Mon Sep 17 00:00:00 2001 From: nick-funk Date: Thu, 18 Jan 2024 13:16:16 -0700 Subject: [PATCH 23/28] add further notes to wordpress readme --- wordpress/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/wordpress/README.md b/wordpress/README.md index 96ce91bbc4..99c28f8280 100644 --- a/wordpress/README.md +++ b/wordpress/README.md @@ -11,9 +11,11 @@ This is a dev environment for testing out the [talk-wp-plugin](https://github.co 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. -- Then navigate to `http://localhost:8081`. +- Navigate to `http://localhost:8081`. - Follow the steps to create a new admin user for the wordpress deployment. @@ -29,5 +31,7 @@ This is a dev environment for testing out the [talk-wp-plugin](https://github.co - 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 From 21c59f0d6b24704d445781b095ed6c113d85bc8e Mon Sep 17 00:00:00 2001 From: Admin Date: Fri, 19 Jan 2024 15:32:48 -0800 Subject: [PATCH 24/28] updating license to current year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 0d96005a9f0a1ddf6b9a3452188c494c4aa530b7 Mon Sep 17 00:00:00 2001 From: rmens Date: Mon, 22 Jan 2024 11:53:37 +0100 Subject: [PATCH 25/28] Add strings for NL --- locales/nl-NL/account.ftl | 16 +++++++---- locales/nl-NL/common.ftl | 44 ++++++++++++++++++++++++++++- locales/nl-NL/stream.ftl | 59 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 6 deletions(-) diff --git a/locales/nl-NL/account.ftl b/locales/nl-NL/account.ftl index f79256a416..61750ae2c7 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 = Bevestitings 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 From 911c45f0e507b75c2c70eea49e1a714f8051f173 Mon Sep 17 00:00:00 2001 From: rmens Date: Mon, 22 Jan 2024 22:25:49 +0100 Subject: [PATCH 26/28] Typo --- locales/nl-NL/account.ftl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/nl-NL/account.ftl b/locales/nl-NL/account.ftl index 61750ae2c7..aec4da579f 100644 --- a/locales/nl-NL/account.ftl +++ b/locales/nl-NL/account.ftl @@ -26,7 +26,7 @@ resetPassword-missingResetToken = De markering voor herstel lijkt te ontbreken. ## Email Confirmation -confirmEmail-emailConfirmation = Bevestitings e-mail +confirmEmail-emailConfirmation = Bevestigings-e-mail confirmEmail-confirmYourEmailAddress = Bevestig je e-mailadres confirmEmail-confirmEmail = From 97d845a6768284de9ae06fc403934d5f923f0367 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Tue, 23 Jan 2024 13:55:37 -0500 Subject: [PATCH 27/28] linting fixes --- .../admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx | 2 +- .../UserHistoryDrawer/DeleteAccountPopoverContainer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx index 5fa4fe0d84..06eab24eb2 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopover.tsx @@ -39,7 +39,7 @@ const DeleteAccountPopover: FunctionComponent = ({ await scheduleAccountDeletion({ userID }); } catch (e) { if (e.message) { - setRequestDeletionError(e.message); + setRequestDeletionError(e.message as string); } } }, [userID, scheduleAccountDeletion, setRequestDeletionError]); diff --git a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx index 28b1f2c038..df50e585d1 100644 --- a/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx +++ b/client/src/core/client/admin/components/UserHistoryDrawer/DeleteAccountPopoverContainer.tsx @@ -45,7 +45,7 @@ const DeleteAccountPopoverContainer: FunctionComponent = ({ user }) => { await cancelScheduledAccountDeletion({ userID: user.id }); } catch (e) { if (e.message) { - setCancelDeletionError(e.message); + setCancelDeletionError(e.message as string); } } }, [user.id, cancelScheduledAccountDeletion, setCancelDeletionError]); From 0f95f4a7ad8a4efbed114f4da948b90bf40394d7 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Tue, 23 Jan 2024 14:44:15 -0500 Subject: [PATCH 28/28] bump to 8.7.2 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- common/package-lock.json | 4 ++-- common/package.json | 2 +- config/package-lock.json | 4 ++-- config/package.json | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) 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/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/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": [