diff --git a/client/src/core/client/admin/routes/Configure/sections/General/DSAConfigContainer.tsx b/client/src/core/client/admin/routes/Configure/sections/General/DSAConfigContainer.tsx index 542058d89e..46665a943d 100644 --- a/client/src/core/client/admin/routes/Configure/sections/General/DSAConfigContainer.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/General/DSAConfigContainer.tsx @@ -2,8 +2,11 @@ import { Localized } from "@fluent/react/compat"; import React, { FunctionComponent } from "react"; import { graphql } from "react-relay"; +import { withFragmentContainer } from "coral-framework/lib/relay"; import { FieldSet, FormField, HelperText, Label } from "coral-ui/components/v2"; +import { DSAConfigContainer_settings } from "coral-admin/__generated__/DSAConfigContainer_settings.graphql"; + import ConfigBox from "../../ConfigBox"; import Header from "../../Header"; import OnOffField from "../../OnOffField"; @@ -14,16 +17,24 @@ graphql` fragment DSAConfigContainer_formValues on Settings { dsa { enabled - methodOfRedress + methodOfRedress { + method + url + email + } } } `; interface Props { disabled: boolean; + settings: DSAConfigContainer_settings; } -export const DSAConfigContainer: FunctionComponent = ({ disabled }) => { +export const DSAConfigContainer: FunctionComponent = ({ + disabled, + settings, +}) => { return ( = ({ disabled }) => { ); }; + +const enhanced = withFragmentContainer({ + settings: graphql` + fragment DSAConfigContainer_settings on Settings { + dsa { + enabled + methodOfRedress { + method + url + email + } + } + } + `, +})(DSAConfigContainer); + +export default enhanced; diff --git a/client/src/core/client/admin/routes/Configure/sections/General/DSAMethodOfRedressOptions.css b/client/src/core/client/admin/routes/Configure/sections/General/DSAMethodOfRedressOptions.css new file mode 100644 index 0000000000..bcb0e3e1a2 --- /dev/null +++ b/client/src/core/client/admin/routes/Configure/sections/General/DSAMethodOfRedressOptions.css @@ -0,0 +1,6 @@ +.textInput { + margin-top: var(--spacing-2); + margin-bottom: var(--spacing-2); + margin-left: var(--spacing-5); + margin-right: var(--spacing-6); +} diff --git a/client/src/core/client/admin/routes/Configure/sections/General/DSAMethodOfRedressOptions.tsx b/client/src/core/client/admin/routes/Configure/sections/General/DSAMethodOfRedressOptions.tsx index b1b57d89a7..4470c834cc 100644 --- a/client/src/core/client/admin/routes/Configure/sections/General/DSAMethodOfRedressOptions.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/General/DSAMethodOfRedressOptions.tsx @@ -1,13 +1,29 @@ import { Localized } from "@fluent/react/compat"; -import React, { FunctionComponent } from "react"; +import React, { + ChangeEvent, + FunctionComponent, + useCallback, + useState, +} from "react"; import { Field } from "react-final-form"; -import { Validator } from "coral-framework/lib/validation"; +import { formatEmpty, parseEmptyAsNull } from "coral-framework/lib/form"; +import { + validateEmail, + validateURL, + Validator, +} from "coral-framework/lib/validation"; import { RadioButton } from "coral-ui/components/v2"; +import { DSA_METHOD_OF_REDRESS } from "coral-admin/__generated__/DSAConfigContainer_formValues.graphql"; + +import TextFieldWithValidation from "../../TextFieldWithValidation"; + +import styles from "./DSAMethodOfRedressOptions.css"; + interface Props { + defaultMethod: DSA_METHOD_OF_REDRESS | null; validate?: Validator; - name: string; disabled: boolean; format?: (value: any, name: string) => any; testIDs?: { @@ -18,23 +34,23 @@ interface Props { } export enum DSAMethodOfRedress { - None = "NONE", - Email = "EMAIL", + NONE = "NONE", + EMAIL = "EMAIL", URL = "URL", } export const parseVal = (v: any, name: string) => { - if (v === DSAMethodOfRedress.None) { - return DSAMethodOfRedress.None; + if (v === DSAMethodOfRedress.NONE) { + return DSAMethodOfRedress.NONE; } - if (v === DSAMethodOfRedress.Email) { - return DSAMethodOfRedress.Email; + if (v === DSAMethodOfRedress.EMAIL) { + return DSAMethodOfRedress.EMAIL; } if (v === DSAMethodOfRedress.URL) { return DSAMethodOfRedress.URL; } - return DSAMethodOfRedress.None; + return DSAMethodOfRedress.NONE; }; export const format = (v: string, name: string) => { @@ -42,59 +58,145 @@ export const format = (v: string, name: string) => { }; const DSAMethodOfRedressOptions: FunctionComponent = ({ - name, + defaultMethod, disabled, className, -}) => ( -
- - {({ input }) => ( - - - None - - - )} - - - {({ input }) => ( - - - Email - - - )} - - - {({ input }) => { - return ( - - - URL +}) => { + const [mode, setMode] = useState(defaultMethod); + + const onChange = useCallback((ev: ChangeEvent) => { + if (ev.target?.value === DSAMethodOfRedress.EMAIL) { + setMode(DSAMethodOfRedress.EMAIL); + } else if (ev.target?.value === DSAMethodOfRedress.URL) { + setMode(DSAMethodOfRedress.URL); + } else { + setMode(null); + } + }, []); + + return ( +
+ + {({ input }) => ( + { + input.onChange(ev); + onChange(ev); + }} + > + + None + + + )} + + + {({ input }) => ( + { + input.onChange(ev); + onChange(ev); + }} + > + + Email - ); - }} - -
-); + )} +
+ {mode === DSAMethodOfRedress.EMAIL && ( + + {({ input, meta }) => ( +
+ +
+ )} +
+ )} + + {({ input }) => { + return ( + { + input.onChange(ev); + onChange(ev); + }} + > + + URL + + + ); + }} + + {mode === DSAMethodOfRedress.URL && ( + + {({ input, meta }) => ( +
+ +
+ )} +
+ )} +
+ ); +}; export default DSAMethodOfRedressOptions; diff --git a/client/src/core/client/admin/routes/Configure/sections/General/GeneralConfigContainer.tsx b/client/src/core/client/admin/routes/Configure/sections/General/GeneralConfigContainer.tsx index 28143b659d..8805ca822a 100644 --- a/client/src/core/client/admin/routes/Configure/sections/General/GeneralConfigContainer.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/General/GeneralConfigContainer.tsx @@ -10,7 +10,7 @@ import { HorizontalGutter } from "coral-ui/components/v2"; import { GeneralConfigContainer_settings as SettingsData } from "coral-admin/__generated__/GeneralConfigContainer_settings.graphql"; -import { DSAConfigContainer } from "../General/DSAConfigContainer"; +import DSAConfigContainer from "../General/DSAConfigContainer"; import AnnouncementConfigContainer from "./AnnouncementConfigContainer"; import BadgeConfig from "./BadgeConfig"; import ClosedStreamMessageConfig from "./ClosedStreamMessageConfig"; @@ -48,7 +48,7 @@ const GeneralConfigContainer: React.FunctionComponent = ({ className={styles.root} > - + @@ -75,6 +75,7 @@ const enhanced = withFragmentContainer({ ...FlattenRepliesConfig_formValues @relay(mask: false) ...LocaleConfig_formValues @relay(mask: false) ...DSAConfigContainer_formValues @relay(mask: false) + ...DSAConfigContainer_settings ...GuidelinesConfig_formValues @relay(mask: false) ...CommentLengthConfig_formValues @relay(mask: false) ...CommentEditingConfig_formValues @relay(mask: false) diff --git a/client/src/core/client/admin/test/fixtures.ts b/client/src/core/client/admin/test/fixtures.ts index d715dc63fc..e3ed99a4c3 100644 --- a/client/src/core/client/admin/test/fixtures.ts +++ b/client/src/core/client/admin/test/fixtures.ts @@ -236,7 +236,9 @@ export const settings = createFixture({ }, dsa: { enabled: false, - methodOfRedress: GQLDSA_METHOD_OF_REDRESS.NONE, + methodOfRedress: { + method: GQLDSA_METHOD_OF_REDRESS.NONE, + }, }, }); diff --git a/client/src/core/client/stream/tabs/Notifications/NotificationsContainer.css b/client/src/core/client/stream/tabs/Notifications/NotificationsContainer.css index fc49a10ff4..ccfd84d7c8 100644 --- a/client/src/core/client/stream/tabs/Notifications/NotificationsContainer.css +++ b/client/src/core/client/stream/tabs/Notifications/NotificationsContainer.css @@ -5,7 +5,25 @@ .title { font-family: var(--font-family-primary); font-weight: var(--font-weight-primary-semi-bold); - font-size: var(--font-size-3); + font-size: var(--font-size-4); word-break: break-word; + + margin-bottom: var(--spacing-2); +} + +.methodOfRedress { + padding-left: var(--spacing-4); + padding-right: var(--spacing-4); + padding-top: var(--spacing-2); + padding-bottom: var(--spacing-2); + + background-color: var(--palette-primary-100); + + font-family: var(--font-family-primary); + color: var(--palette-text-200); + font-weight: var(--font-weight-primary-regular); + font-size: var(--font-size-3); + + margin-bottom: var(--spacing-2); } diff --git a/client/src/core/client/stream/tabs/Notifications/NotificationsContainer.tsx b/client/src/core/client/stream/tabs/Notifications/NotificationsContainer.tsx index bafb554fd6..b50dc9c76d 100644 --- a/client/src/core/client/stream/tabs/Notifications/NotificationsContainer.tsx +++ b/client/src/core/client/stream/tabs/Notifications/NotificationsContainer.tsx @@ -3,6 +3,7 @@ import React, { FunctionComponent, useCallback, useEffect } from "react"; import { graphql } from "react-relay"; import { useLocal, withFragmentContainer } from "coral-framework/lib/relay"; +import { GQLDSA_METHOD_OF_REDRESS } from "coral-framework/schema"; import { UserBoxContainer } from "coral-stream/common/UserBox"; import { NotificationsContainer_settings } from "coral-stream/__generated__/NotificationsContainer_settings.graphql"; @@ -44,6 +45,36 @@ const NotificationsContainer: FunctionComponent = ({
Notifications
+
+ {settings.dsa.methodOfRedress.method === + GQLDSA_METHOD_OF_REDRESS.NONE && ( + + All moderation decisions are final and cannot be appealed + + )} + {settings.dsa.methodOfRedress.method === + GQLDSA_METHOD_OF_REDRESS.EMAIL && ( + + {`To appeal a decision that appears here please contact ${settings.dsa.methodOfRedress.email}`} + + )} + {settings.dsa.methodOfRedress.method === + GQLDSA_METHOD_OF_REDRESS.URL && ( + + {`To appeal a decision that appears here please visit ${settings.dsa.methodOfRedress.url}`} + + )} +
); @@ -59,6 +90,13 @@ const enhanced = withFragmentContainer({ settings: graphql` fragment NotificationsContainer_settings on Settings { ...UserBoxContainer_settings + dsa { + methodOfRedress { + method + email + url + } + } } `, })(NotificationsContainer); diff --git a/client/src/core/client/stream/test/fixtures.ts b/client/src/core/client/stream/test/fixtures.ts index b4b2d19d7b..8e029fa52d 100644 --- a/client/src/core/client/stream/test/fixtures.ts +++ b/client/src/core/client/stream/test/fixtures.ts @@ -3,6 +3,7 @@ import { GQLComment, GQLCOMMENT_STATUS, GQLDIGEST_FREQUENCY, + GQLDSA_METHOD_OF_REDRESS, GQLFEATURE_FLAG, GQLMODERATION_MODE, GQLSettings, @@ -137,6 +138,9 @@ export const settings = createFixture({ }, dsa: { enabled: false, + methodOfRedress: { + method: GQLDSA_METHOD_OF_REDRESS.NONE, + }, }, }); diff --git a/locales/en-US/stream.ftl b/locales/en-US/stream.ftl index 84eb29f66c..fb367ff473 100644 --- a/locales/en-US/stream.ftl +++ b/locales/en-US/stream.ftl @@ -1077,4 +1077,11 @@ notifications-rejectionReason-unknown = Unknown notifications-reportDecisionMade-legal = On { $date } you reported a comment written by { $author } for containing illegal content. After reviewing your report, our moderation team has decided this comment does not appear to contain illegal content. notifications-reportDecisionMade-illegal = - On { $date } you reported a comment written by { $author } for containing illegal content. After reviewing your report, our moderation team has decided this comment does contain illegal content. \ No newline at end of file + On { $date } you reported a comment written by { $author } for containing illegal content. After reviewing your report, our moderation team has decided this comment does contain illegal content. + +notifications-methodOfRedress-none = + All moderation decisions are final and cannot be appealed +notifications-methodOfRedress-email = + To appeal a decision that appears here please contact { $email } +notifications-methodOfRedress-url = + To appeal a decision that appears here please visit { $url } \ No newline at end of file diff --git a/server/src/core/server/graph/resolvers/DSAConfiguration.ts b/server/src/core/server/graph/resolvers/DSAConfiguration.ts index 2b92649f46..1edf2f6930 100644 --- a/server/src/core/server/graph/resolvers/DSAConfiguration.ts +++ b/server/src/core/server/graph/resolvers/DSAConfiguration.ts @@ -5,12 +5,14 @@ import { GQLDSAConfigurationTypeResolver, } from "coral-server/graph/schema/__generated__/types"; -export const DSAConfiguration: GQLDSAConfigurationTypeResolver = +export const DSAConfiguration: GQLDSAConfigurationTypeResolver = { enabled: (config, args, { tenant }) => tenant.dsa && tenant.dsa.enabled ? tenant.dsa.enabled : false, methodOfRedress: (config, args, { tenant }) => - tenant.dsa && tenant.dsa.methodOfRedress - ? tenant.dsa.methodOfRedress - : GQLDSA_METHOD_OF_REDRESS.NONE, + tenant.dsa?.methodOfRedress ?? { + method: GQLDSA_METHOD_OF_REDRESS.NONE, + email: "", + url: "", + }, }; diff --git a/server/src/core/server/graph/resolvers/DSAMethodOfRedressConfiguration.ts b/server/src/core/server/graph/resolvers/DSAMethodOfRedressConfiguration.ts new file mode 100644 index 0000000000..a56cf99c8b --- /dev/null +++ b/server/src/core/server/graph/resolvers/DSAMethodOfRedressConfiguration.ts @@ -0,0 +1,13 @@ +import { + GQLDSA_METHOD_OF_REDRESS, + GQLDSAMethodOfRedressConfigurationTypeResolver, +} from "coral-server/graph/schema/__generated__/types"; + +export const DSAMethodOfRedressConfiguration: GQLDSAMethodOfRedressConfigurationTypeResolver = + { + method: (config, args, { tenant }) => + tenant.dsa?.methodOfRedress?.method ?? GQLDSA_METHOD_OF_REDRESS.NONE, + email: (config, args, { tenant }) => + tenant.dsa?.methodOfRedress?.email ?? "", + url: (config, args, { tenant }) => tenant.dsa?.methodOfRedress?.url ?? "", + }; diff --git a/server/src/core/server/graph/resolvers/index.ts b/server/src/core/server/graph/resolvers/index.ts index 3f2927f007..03f237aa3f 100644 --- a/server/src/core/server/graph/resolvers/index.ts +++ b/server/src/core/server/graph/resolvers/index.ts @@ -25,6 +25,8 @@ import { CommentReplyCreatedPayload } from "./CommentReplyCreatedPayload"; import { CommentRevision } from "./CommentRevision"; import { CommentStatusUpdatedPayload } from "./CommentStatusUpdatedPayload"; import { DisableCommenting } from "./DisableCommenting"; +import { DSAConfiguration } from "./DSAConfiguration"; +import { DSAMethodOfRedressConfiguration } from "./DSAMethodOfRedressConfiguration"; import { DSAReport } from "./DSAReport"; import { EditInfo } from "./EditInfo"; import { EmailDomain } from "./EmailDomain"; @@ -171,6 +173,8 @@ const Resolvers: GQLResolver = { AuthenticationTargetFilter, Notification, NotificationDSAReportDetails, + DSAConfiguration, + DSAMethodOfRedressConfiguration, }; export default Resolvers; diff --git a/server/src/core/server/graph/schema/schema.graphql b/server/src/core/server/graph/schema/schema.graphql index d0406f1aed..eeba2131d7 100644 --- a/server/src/core/server/graph/schema/schema.graphql +++ b/server/src/core/server/graph/schema/schema.graphql @@ -1954,6 +1954,24 @@ enum DSA_METHOD_OF_REDRESS { URL } +type DSAMethodOfRedressConfiguration { + """ + method defines the type of redress that is available to the + users about DSA decisions. + """ + method: DSA_METHOD_OF_REDRESS! + + """ + email is the email used when method is set to EMAIL. + """ + email: String + + """ + url is the url that is used when method is set to URL. + """ + url: String +} + type DSAConfiguration { """ enabled when true turns on the European Union DSA compliance @@ -1965,7 +1983,7 @@ type DSAConfiguration { methodOfRedress lets users know if and how they can appeal a moderation decision """ - methodOfRedress: DSA_METHOD_OF_REDRESS! + methodOfRedress: DSAMethodOfRedressConfiguration! } """ @@ -6098,6 +6116,28 @@ input FlairBadgeConfigurationInput { flairBadgeURLs: [String!] } +""" +DSAMethodOfRedressConfigurationInput specifies the methods of redress and +their configuration values for users disputing DSA reporting decisions. +""" +input DSAMethodOfRedressConfigurationInput { + """ + method defines the type of redress that is available to the + users about DSA decisions. + """ + method: DSA_METHOD_OF_REDRESS + + """ + email is the email used when method is set to EMAIL. + """ + email: String + + """ + url is the url that is used when method is set to URL. + """ + url: String +} + """ DSAConfigurationInput specifies the configuration for DSA European Union moderation and reporting features. @@ -6113,7 +6153,7 @@ input DSAConfigurationInput { methodOfRedress lets users know if and how they can appeal a moderation decision """ - methodOfRedress: DSA_METHOD_OF_REDRESS + methodOfRedress: DSAMethodOfRedressConfigurationInput } """ diff --git a/server/src/core/server/models/tenant/tenant.ts b/server/src/core/server/models/tenant/tenant.ts index 8f3443e63e..8191e47795 100644 --- a/server/src/core/server/models/tenant/tenant.ts +++ b/server/src/core/server/models/tenant/tenant.ts @@ -302,7 +302,9 @@ export async function createTenant( }, dsa: { enabled: false, - methodOfRedress: GQLDSA_METHOD_OF_REDRESS.NONE, + methodOfRedress: { + method: GQLDSA_METHOD_OF_REDRESS.NONE, + }, }, }; diff --git a/server/src/core/server/test/fixtures.ts b/server/src/core/server/test/fixtures.ts index f2f604af66..b27fa72372 100644 --- a/server/src/core/server/test/fixtures.ts +++ b/server/src/core/server/test/fixtures.ts @@ -187,7 +187,9 @@ export const createTenantFixture = ( emailDomainModeration: [], dsa: { enabled: false, - methodOfRedress: GQLDSA_METHOD_OF_REDRESS.NONE, + methodOfRedress: { + method: GQLDSA_METHOD_OF_REDRESS.NONE, + }, }, embeddedComments: { allowReplies: true,