diff --git a/client/src/graphql/generated.ts b/client/src/graphql/generated.ts index afc2273..8ff77a0 100644 --- a/client/src/graphql/generated.ts +++ b/client/src/graphql/generated.ts @@ -2346,6 +2346,7 @@ export type GQLModeratorSafetySettingsInput = { readonly moderatorSafetyBlurLevel: Scalars['Int']['input']; readonly moderatorSafetyGrayscale: Scalars['Boolean']['input']; readonly moderatorSafetyMuteVideo: Scalars['Boolean']['input']; + readonly moderatorSafetySepia: Scalars['Boolean']['input']; }; export const GQLMrtClearReportsDisposition = { @@ -5046,6 +5047,7 @@ export type GQLUserInterfacePreferences = { readonly moderatorSafetyBlurLevel: Scalars['Int']['output']; readonly moderatorSafetyGrayscale: Scalars['Boolean']['output']; readonly moderatorSafetyMuteVideo: Scalars['Boolean']['output']; + readonly moderatorSafetySepia: Scalars['Boolean']['output']; readonly mrtChartConfigurations: ReadonlyArray; }; @@ -12263,6 +12265,7 @@ export type GQLManualReviewSafetySettingsQuery = { readonly moderatorSafetyMuteVideo: boolean; readonly moderatorSafetyGrayscale: boolean; readonly moderatorSafetyBlurLevel: number; + readonly moderatorSafetySepia: boolean; }; } | null; }; @@ -24776,6 +24779,7 @@ export type GQLPersonalSafetySettingsQuery = { readonly moderatorSafetyMuteVideo: boolean; readonly moderatorSafetyGrayscale: boolean; readonly moderatorSafetyBlurLevel: number; + readonly moderatorSafetySepia: boolean; }; } | null; }; @@ -25253,6 +25257,7 @@ export type GQLOrgDefaultSafetySettingsQuery = { readonly moderatorSafetyMuteVideo: boolean; readonly moderatorSafetyGrayscale: boolean; readonly moderatorSafetyBlurLevel: number; + readonly moderatorSafetySepia: boolean; }; } | null; }; @@ -34364,6 +34369,7 @@ export const GQLManualReviewSafetySettingsDocument = gql` moderatorSafetyMuteVideo moderatorSafetyGrayscale moderatorSafetyBlurLevel + moderatorSafetySepia } } } @@ -43008,6 +43014,7 @@ export const GQLPersonalSafetySettingsDocument = gql` moderatorSafetyMuteVideo moderatorSafetyGrayscale moderatorSafetyBlurLevel + moderatorSafetySepia } } } @@ -45198,6 +45205,7 @@ export const GQLOrgDefaultSafetySettingsDocument = gql` moderatorSafetyMuteVideo moderatorSafetyGrayscale moderatorSafetyBlurLevel + moderatorSafetySepia } } } diff --git a/client/src/models/safetySettings.test.ts b/client/src/models/safetySettings.test.ts new file mode 100644 index 0000000..ff04930 --- /dev/null +++ b/client/src/models/safetySettings.test.ts @@ -0,0 +1,77 @@ +import { + colorSchemeClassName, + colorSchemeFromPreferences, + preferencesFromColorScheme, +} from './safetySettings'; + +describe('safetySettings color scheme', () => { + it('maps boolean preferences to a color scheme', () => { + expect( + colorSchemeFromPreferences({ + moderatorSafetyGrayscale: true, + moderatorSafetySepia: false, + }), + ).toBe('GRAYSCALE'); + expect( + colorSchemeFromPreferences({ + moderatorSafetyGrayscale: false, + moderatorSafetySepia: true, + }), + ).toBe('SEPIA'); + expect( + colorSchemeFromPreferences({ + moderatorSafetyGrayscale: false, + moderatorSafetySepia: false, + }), + ).toBe('NONE'); + }); + + it('prefers grayscale if both stored flags are set', () => { + expect( + colorSchemeFromPreferences({ + moderatorSafetyGrayscale: true, + moderatorSafetySepia: true, + }), + ).toBe('GRAYSCALE'); + }); + + it('maps a color scheme back to mutually exclusive booleans', () => { + expect(preferencesFromColorScheme('GRAYSCALE')).toEqual({ + moderatorSafetyGrayscale: true, + moderatorSafetySepia: false, + }); + expect(preferencesFromColorScheme('SEPIA')).toEqual({ + moderatorSafetyGrayscale: false, + moderatorSafetySepia: true, + }); + expect(preferencesFromColorScheme('NONE')).toEqual({ + moderatorSafetyGrayscale: false, + moderatorSafetySepia: false, + }); + }); + + it('round-trips every scheme', () => { + for (const scheme of ['NONE', 'GRAYSCALE', 'SEPIA'] as const) { + expect( + colorSchemeFromPreferences(preferencesFromColorScheme(scheme)), + ).toBe(scheme); + } + }); + + it('maps a color scheme to its Tailwind class', () => { + expect(colorSchemeClassName('GRAYSCALE')).toBe('grayscale'); + expect(colorSchemeClassName('SEPIA')).toBe('sepia'); + expect(colorSchemeClassName('NONE')).toBe(''); + }); + + it('never yields both filter classes even if both flags are set', () => { + expect( + colorSchemeClassName( + colorSchemeFromPreferences({ + moderatorSafetyGrayscale: true, + moderatorSafetySepia: true, + }), + ), + ).toBe('grayscale'); + }); +}); diff --git a/client/src/models/safetySettings.ts b/client/src/models/safetySettings.ts new file mode 100644 index 0000000..71d7266 --- /dev/null +++ b/client/src/models/safetySettings.ts @@ -0,0 +1,62 @@ +export const MODERATOR_SAFETY_COLOR_SCHEMES = [ + 'NONE', + 'GRAYSCALE', + 'SEPIA', +] as const; + +export type ModeratorSafetyColorScheme = + (typeof MODERATOR_SAFETY_COLOR_SCHEMES)[number]; + +export const MODERATOR_SAFETY_COLOR_SCHEME_LABELS: Record< + ModeratorSafetyColorScheme, + string +> = { + NONE: 'None', + GRAYSCALE: 'Grayscale', + SEPIA: 'Sepia', +}; + +// The API stores the color scheme as two independent booleans +// (moderatorSafetyGrayscale / moderatorSafetySepia) so the schema stays +// backwards-compatible; the UI models them as one mutually exclusive scheme. +// Grayscale wins if both flags are somehow set — the UI only ever writes one. +export function colorSchemeFromPreferences(preferences: { + moderatorSafetyGrayscale: boolean; + moderatorSafetySepia: boolean; +}): ModeratorSafetyColorScheme { + if (preferences.moderatorSafetyGrayscale) { + return 'GRAYSCALE'; + } + if (preferences.moderatorSafetySepia) { + return 'SEPIA'; + } + return 'NONE'; +} + +// Tailwind filter class for a resolved color scheme. Deriving classes from the +// resolved scheme (rather than the raw booleans) keeps the "grayscale wins" +// invariant even if both flags are somehow set. +export function colorSchemeClassName( + colorScheme: ModeratorSafetyColorScheme, +): string { + switch (colorScheme) { + case 'GRAYSCALE': + return 'grayscale'; + case 'SEPIA': + return 'sepia'; + case 'NONE': + return ''; + } +} + +export function preferencesFromColorScheme( + colorScheme: ModeratorSafetyColorScheme, +): { + moderatorSafetyGrayscale: boolean; + moderatorSafetySepia: boolean; +} { + return { + moderatorSafetyGrayscale: colorScheme === 'GRAYSCALE', + moderatorSafetySepia: colorScheme === 'SEPIA', + }; +} diff --git a/client/src/webpages/dashboard/mrt/ManualReviewSafetySettings.tsx b/client/src/webpages/dashboard/mrt/ManualReviewSafetySettings.tsx index 4044e57..b1649c0 100644 --- a/client/src/webpages/dashboard/mrt/ManualReviewSafetySettings.tsx +++ b/client/src/webpages/dashboard/mrt/ManualReviewSafetySettings.tsx @@ -1,4 +1,11 @@ import { Label } from '@/coop-ui/Label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/coop-ui/Select'; import { Slider } from '@/coop-ui/Slider'; import { Switch } from '@/coop-ui/Switch'; import { gql } from '@apollo/client'; @@ -15,6 +22,14 @@ import { useGQLSetModeratorSafetySettingsMutation, } from '../../../graphql/generated'; import GoldenRetrieverPuppies from '../../../images/GoldenRetrieverPuppies.png'; +import { + colorSchemeClassName, + colorSchemeFromPreferences, + MODERATOR_SAFETY_COLOR_SCHEME_LABELS, + MODERATOR_SAFETY_COLOR_SCHEMES, + preferencesFromColorScheme, + type ModeratorSafetyColorScheme, +} from '../../../models/safetySettings'; import { BLUR_LEVELS, BlurStrength, @@ -27,6 +42,7 @@ gql` moderatorSafetyMuteVideo moderatorSafetyGrayscale moderatorSafetyBlurLevel + moderatorSafetySepia } } } @@ -47,10 +63,12 @@ export default function ManualReviewSafetySettings() { moderatorSafetyBlurLevel: BlurStrength; moderatorSafetyGrayscale: boolean; moderatorSafetyMuteVideo: boolean; + moderatorSafetySepia: boolean; }>({ moderatorSafetyBlurLevel: 2, moderatorSafetyGrayscale: true, moderatorSafetyMuteVideo: true, + moderatorSafetySepia: false, }); const [notificationApi, notificationContextHolder] = notification.useNotification(); @@ -76,11 +94,13 @@ export default function ManualReviewSafetySettings() { moderatorSafetyMuteVideo, moderatorSafetyGrayscale, moderatorSafetyBlurLevel, + moderatorSafetySepia, } = data.me.interfacePreferences; setSettings({ moderatorSafetyMuteVideo, moderatorSafetyGrayscale, moderatorSafetyBlurLevel: moderatorSafetyBlurLevel as BlurStrength, + moderatorSafetySepia, }); }, [data?.me?.interfacePreferences]); @@ -123,27 +143,35 @@ export default function ManualReviewSafetySettings() { step={1} /> -
-
- - setSettings({ - ...settings, - moderatorSafetyGrayscale: value, - }) - } - checked={settings.moderatorSafetyGrayscale} - /> - -
+
+ +
setSettings({ ...settings, @@ -161,7 +189,7 @@ export default function ManualReviewSafetySettings() { settings.moderatorSafetyBlurLevel != null ? BLUR_LEVELS[settings.moderatorSafetyBlurLevel] : 'blur-sm' - } ${settings.moderatorSafetyGrayscale ? 'grayscale' : ''}`} + } ${colorSchemeClassName(colorSchemeFromPreferences(settings))}`} alt="puppies" src={GoldenRetrieverPuppies} /> diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx index 954849b..02a72e9 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx @@ -25,25 +25,35 @@ export default function IframeContentDisplayComponent(props: { blur: boolean; grayscale: boolean; shouldTranslate: boolean; + sepia: boolean; }>({ blur: true, grayscale: false, shouldTranslate: false, + sepia: false, }); - const { blur, grayscale, shouldTranslate } = state; + const { blur, grayscale, shouldTranslate, sepia } = state; const { loading, data } = useGQLPersonalSafetySettingsQuery(); - const { moderatorSafetyBlurLevel = 2, moderatorSafetyGrayscale = true } = - data?.me?.interfacePreferences ?? {}; + const { + moderatorSafetyBlurLevel = 2, + moderatorSafetyGrayscale = true, + moderatorSafetySepia = false, + } = data?.me?.interfacePreferences ?? {}; useEffect(() => { setState({ blur: moderatorSafetyBlurLevel !== 0, grayscale: moderatorSafetyGrayscale, shouldTranslate: false, + sepia: moderatorSafetySepia, }); - }, [moderatorSafetyBlurLevel, moderatorSafetyGrayscale]); + }, [ + moderatorSafetyBlurLevel, + moderatorSafetyGrayscale, + moderatorSafetySepia, + ]); useEffect(() => { // Translation status messages come from the content proxy. With no proxy @@ -77,6 +87,7 @@ export default function IframeContentDisplayComponent(props: { blur: blur ? moderatorSafetyBlurLevel : 0, grayscale, shouldTranslate, + sepia, }, proxyUrl, ); @@ -93,6 +104,7 @@ export default function IframeContentDisplayComponent(props: { blur: blur ? moderatorSafetyBlurLevel : 0, grayscale, shouldTranslate, + sepia, }, proxyUrl, ); @@ -116,6 +128,7 @@ export default function IframeContentDisplayComponent(props: { shouldTranslate, contentProxyUrl, isIframeLoading, + sepia, ], ); diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobContentBlurableImage.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobContentBlurableImage.tsx index 973a9db..32f58cf 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobContentBlurableImage.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobContentBlurableImage.tsx @@ -14,6 +14,7 @@ export default function ManualReviewJobContentBlurableImage(props: { blurStrength?: BlurStrength; grayscale?: boolean; disableZoom?: boolean; + sepia?: boolean; }; onError?: () => void; }) { @@ -25,6 +26,7 @@ export default function ManualReviewJobContentBlurableImage(props: { blurStrength = 0, grayscale = false, disableZoom = false, + sepia = false, } = options ?? {}; const [clicked, setClicked] = useState(false); @@ -49,7 +51,7 @@ export default function ManualReviewJobContentBlurableImage(props: { setClicked(true)} diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobFieldsComponent.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobFieldsComponent.tsx index 3cf58a5..347011a 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobFieldsComponent.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobFieldsComponent.tsx @@ -278,6 +278,7 @@ function TableRowComponent(props: { ? (safetySettings.moderatorSafetyBlurLevel as BlurStrength) : (2 as const), grayscale: safetySettings?.moderatorSafetyGrayscale ?? false, + sepia: safetySettings?.moderatorSafetySepia ?? false, }} /> {label ?
{label}
: null} @@ -352,6 +353,7 @@ function TableRowComponent(props: { ? (safetySettings.moderatorSafetyBlurLevel as BlurStrength) : (2 as const), grayscale: safetySettings?.moderatorSafetyGrayscale ?? false, + sepia: safetySettings?.moderatorSafetySepia ?? false, }} /> {label ?
{label}
: null} diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/v2/ncmec/NCMECMediaViewer.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/v2/ncmec/NCMECMediaViewer.tsx index 5ea0d96..c24b7b2 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/v2/ncmec/NCMECMediaViewer.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/v2/ncmec/NCMECMediaViewer.tsx @@ -1,6 +1,19 @@ import { Label } from '@/coop-ui/Label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/coop-ui/Select'; import { Slider } from '@/coop-ui/Slider'; -import { Switch } from '@/coop-ui/Switch'; +import { + colorSchemeFromPreferences, + MODERATOR_SAFETY_COLOR_SCHEME_LABELS, + MODERATOR_SAFETY_COLOR_SCHEMES, + preferencesFromColorScheme, + type ModeratorSafetyColorScheme, +} from '@/models/safetySettings'; import { SearchOutlined } from '@ant-design/icons'; import { useEffect, useState } from 'react'; @@ -84,10 +97,12 @@ export default function NCMECMediaViewer(props: { moderatorSafetyBlurLevel: BlurStrength; moderatorSafetyGrayscale: boolean; moderatorSafetyMuteVideo: boolean; + moderatorSafetySepia: boolean; }>({ moderatorSafetyBlurLevel: 2, moderatorSafetyGrayscale: true, moderatorSafetyMuteVideo: true, + moderatorSafetySepia: false, }); const { loading, error, data } = useGQLPersonalSafetySettingsQuery(); @@ -170,7 +185,7 @@ export default function NCMECMediaViewer(props: { shouldBlur ? BLUR_LEVELS[safetySettings.moderatorSafetyBlurLevel] : 0 - } ${safetySettings.moderatorSafetyGrayscale ? 'grayscale' : ''}`} + } ${safetySettings.moderatorSafetyGrayscale ? 'grayscale' : ''} ${safetySettings.moderatorSafetySepia ? 'sepia' : ''}`} alt="" src={mediaId.urlInfo.url} onError={(img) => { @@ -190,7 +205,7 @@ export default function NCMECMediaViewer(props: { isInInspectedView ? 'w-auto' : 'object-scale-down grow max-w-64 max-h-48' - } ${safetySettings.moderatorSafetyGrayscale ? 'grayscale' : ''}`} + } ${safetySettings.moderatorSafetyGrayscale ? 'grayscale' : ''} ${safetySettings.moderatorSafetySepia ? 'sepia' : ''}`} url={mediaId.urlInfo.url} options={{ shouldBlur: @@ -254,18 +269,29 @@ export default function NCMECMediaViewer(props: { />
- + +
)} diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/v2/ncmec/NCMECReviewUser.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/v2/ncmec/NCMECReviewUser.tsx index 7300d31..037a431 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/v2/ncmec/NCMECReviewUser.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/v2/ncmec/NCMECReviewUser.tsx @@ -3,6 +3,7 @@ import { BulbOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; import { gql } from '@apollo/client'; import { ItemIdentifier, MediaKind, TaggedScalar } from '@roostorg/coop-types'; import { Button } from 'antd'; +import clsx from 'clsx'; import pick from 'lodash/pick'; import uniqBy from 'lodash/uniqBy'; import uniqWith from 'lodash/uniqWith'; @@ -446,9 +447,10 @@ export default function NCMECReviewUser( } const { - moderatorSafetyBlurLevel, - moderatorSafetyGrayscale, - moderatorSafetyMuteVideo, + moderatorSafetyBlurLevel = 2 as BlurStrength, + moderatorSafetyGrayscale = true, + moderatorSafetyMuteVideo = true, + moderatorSafetySepia = false, } = data?.me?.interfacePreferences ?? {}; // Compares two pieces of media to determine whether they're the same, based @@ -634,23 +636,29 @@ export default function NCMECReviewUser(
{!loading && moderatorSafetyBlurLevel != null && - moderatorSafetyGrayscale != null ? ( + moderatorSafetyGrayscale != null && + moderatorSafetySepia != null ? ( media.urlInfo.mediaType === 'IMAGE' ? ( ) : ( ({ @@ -228,12 +246,14 @@ export default function AccountSettings() { moderatorSafetyMuteVideo, moderatorSafetyGrayscale, moderatorSafetyBlurLevel, + moderatorSafetySepia, } = safetySettingsData.me.interfacePreferences; setSafetySettings({ moderatorSafetyMuteVideo, moderatorSafetyGrayscale, moderatorSafetyBlurLevel: moderatorSafetyBlurLevel as BlurStrength, + moderatorSafetySepia, }); }, [safetySettingsData?.me?.interfacePreferences]); @@ -298,11 +318,20 @@ export default function AccountSettings() { })); }, []); - const setGrayscalePreference = useCallback( - (moderatorSafetyGrayscale: boolean): void => + const setMuteVideoPreference = useCallback( + (moderatorSafetyMuteVideo: boolean): void => + setSafetySettings((prevSettings) => ({ + ...prevSettings, + moderatorSafetyMuteVideo, + })), + [], + ); + + const setColorSchemePreference = useCallback( + (colorScheme: ModeratorSafetyColorScheme): void => setSafetySettings((prevSettings) => ({ ...prevSettings, - moderatorSafetyGrayscale, + ...preferencesFromColorScheme(colorScheme), })), [], ); @@ -366,15 +395,6 @@ export default function AccountSettings() { }); }, [currentPassword, newPassword, confirmNewPassword, changePassword]); - const setMuteVideoPreference = useCallback( - (moderatorSafetyMuteVideo: boolean): void => - setSafetySettings((prevSettings) => ({ - ...prevSettings, - moderatorSafetyMuteVideo, - })), - [], - ); - const moderatorSafetyBlurValue = useMemo( () => [safetySettings.moderatorSafetyBlurLevel], [safetySettings.moderatorSafetyBlurLevel], @@ -589,11 +609,24 @@ export default function AccountSettings() { />
- - + +
@@ -608,7 +641,7 @@ export default function AccountSettings() { puppies diff --git a/client/src/webpages/settings/SettingsPage.test.tsx b/client/src/webpages/settings/SettingsPage.test.tsx index e21ac46..b0796e0 100644 --- a/client/src/webpages/settings/SettingsPage.test.tsx +++ b/client/src/webpages/settings/SettingsPage.test.tsx @@ -128,6 +128,7 @@ const wellnessSettingsMock: MockedResponse = { moderatorSafetyBlurLevel: 2, moderatorSafetyGrayscale: true, moderatorSafetyMuteVideo: true, + moderatorSafetySepia: false, }, }, }, @@ -562,12 +563,14 @@ describe('SettingsPage', () => { screen.getByText('Default Wellness Settings'), ).toBeInTheDocument(); expect(screen.getByText('Blur Media')).toBeInTheDocument(); - expect(screen.getByText('Greyscale')).toBeInTheDocument(); + expect(screen.getByText('Color Scheme')).toBeInTheDocument(); expect(screen.getByText('Mute videos')).toBeInTheDocument(); }); + // Grayscale=true / sepia=false in the mock maps to the GRAYSCALE scheme. + expect(screen.getByRole('combobox')).toHaveTextContent('Grayscale'); const switches = screen.getAllByRole('switch'); + expect(switches).toHaveLength(1); expect(switches[0]).toHaveAttribute('aria-checked', 'true'); - expect(switches[1]).toHaveAttribute('aria-checked', 'true'); }); it('calls save mutation with updated settings', async () => { @@ -584,8 +587,9 @@ describe('SettingsPage', () => { variables: { orgDefaultSafetySettings: { moderatorSafetyBlurLevel: 2, - moderatorSafetyGrayscale: false, - moderatorSafetyMuteVideo: true, + moderatorSafetyGrayscale: true, + moderatorSafetyMuteVideo: false, + moderatorSafetySepia: false, }, }, }, @@ -595,7 +599,7 @@ describe('SettingsPage', () => { 'wellness', ); await waitFor(() => { - expect(screen.getByText('Greyscale')).toBeInTheDocument(); + expect(screen.getByText('Mute videos')).toBeInTheDocument(); }); userEvent.click(screen.getAllByRole('switch')[0]); diff --git a/client/src/webpages/settings/tabs/WellnessTab.tsx b/client/src/webpages/settings/tabs/WellnessTab.tsx index fe4c650..dc738f5 100644 --- a/client/src/webpages/settings/tabs/WellnessTab.tsx +++ b/client/src/webpages/settings/tabs/WellnessTab.tsx @@ -1,13 +1,29 @@ import { Button } from '@/coop-ui/Button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/coop-ui/Select'; import { Slider } from '@/coop-ui/Slider'; import { Switch } from '@/coop-ui/Switch'; import { toast } from '@/coop-ui/Toast'; import { Heading, Text } from '@/coop-ui/Typography'; import { + namedOperations, useGQLOrgDefaultSafetySettingsQuery, useGQLSetOrgDefaultSafetySettingsMutation, } from '@/graphql/generated'; import GoldenRetrieverPuppies from '@/images/GoldenRetrieverPuppies.png'; +import { + colorSchemeClassName, + colorSchemeFromPreferences, + MODERATOR_SAFETY_COLOR_SCHEME_LABELS, + MODERATOR_SAFETY_COLOR_SCHEMES, + preferencesFromColorScheme, + type ModeratorSafetyColorScheme, +} from '@/models/safetySettings'; import { gql } from '@apollo/client'; import { useEffect, useState } from 'react'; @@ -28,6 +44,7 @@ gql` moderatorSafetyMuteVideo moderatorSafetyGrayscale moderatorSafetyBlurLevel + moderatorSafetySepia } } } @@ -47,6 +64,7 @@ type SafetySettings = { moderatorSafetyBlurLevel: BlurStrength; moderatorSafetyGrayscale: boolean; moderatorSafetyMuteVideo: boolean; + moderatorSafetySepia: boolean; }; export default function WellnessTab() { @@ -54,6 +72,7 @@ export default function WellnessTab() { moderatorSafetyBlurLevel: 2, moderatorSafetyGrayscale: true, moderatorSafetyMuteVideo: true, + moderatorSafetySepia: false, }); const { loading, error, data } = useGQLOrgDefaultSafetySettingsQuery({ @@ -64,6 +83,9 @@ export default function WellnessTab() { const [saveSafetySettings, { loading: isSaving }] = useGQLSetOrgDefaultSafetySettingsMutation({ + // The mutation returns no data, so refetch to update the cached + // baseline that hasChanges compares against. + refetchQueries: [namedOperations.Query.OrgDefaultSafetySettings], onCompleted: () => { toast.success('Default wellness settings saved!'); }, @@ -80,11 +102,13 @@ export default function WellnessTab() { moderatorSafetyMuteVideo, moderatorSafetyGrayscale, moderatorSafetyBlurLevel, + moderatorSafetySepia, } = defaultInterfacePreferences; setSafetySettings({ moderatorSafetyMuteVideo, moderatorSafetyGrayscale, moderatorSafetyBlurLevel: moderatorSafetyBlurLevel as BlurStrength, + moderatorSafetySepia, }); }, [defaultInterfacePreferences]); @@ -99,7 +123,8 @@ export default function WellnessTab() { safetySettings.moderatorSafetyGrayscale !== serverPrefs.moderatorSafetyGrayscale || safetySettings.moderatorSafetyMuteVideo !== - serverPrefs.moderatorSafetyMuteVideo; + serverPrefs.moderatorSafetyMuteVideo || + safetySettings.moderatorSafetySepia !== serverPrefs.moderatorSafetySepia; return (
@@ -139,17 +164,30 @@ export default function WellnessTab() {
- Greyscale + Color Scheme - +
@@ -169,7 +207,7 @@ export default function WellnessTab() { puppies diff --git a/db/src/scripts/api-server-pg/2026.07.07T14.27.13.add_sepia.sql b/db/src/scripts/api-server-pg/2026.07.07T14.27.13.add_sepia.sql new file mode 100644 index 0000000..51c8359 --- /dev/null +++ b/db/src/scripts/api-server-pg/2026.07.07T14.27.13.add_sepia.sql @@ -0,0 +1,5 @@ +ALTER TABLE user_management_service.user_interface_settings +ADD COLUMN moderator_safety_sepia boolean; + +ALTER TABLE user_management_service.org_default_user_interface_settings +ADD COLUMN moderator_safety_sepia boolean NOT NULL DEFAULT false; diff --git a/server/graphql/generated.ts b/server/graphql/generated.ts index 4920888..8430dcf 100644 --- a/server/graphql/generated.ts +++ b/server/graphql/generated.ts @@ -2414,6 +2414,7 @@ export type GQLModeratorSafetySettingsInput = { readonly moderatorSafetyBlurLevel: Scalars['Int']['input']; readonly moderatorSafetyGrayscale: Scalars['Boolean']['input']; readonly moderatorSafetyMuteVideo: Scalars['Boolean']['input']; + readonly moderatorSafetySepia: Scalars['Boolean']['input']; }; export const GQLMrtClearReportsDisposition = { @@ -5114,6 +5115,7 @@ export type GQLUserInterfacePreferences = { readonly moderatorSafetyBlurLevel: Scalars['Int']['output']; readonly moderatorSafetyGrayscale: Scalars['Boolean']['output']; readonly moderatorSafetyMuteVideo: Scalars['Boolean']['output']; + readonly moderatorSafetySepia: Scalars['Boolean']['output']; readonly mrtChartConfigurations: ReadonlyArray; }; @@ -14717,6 +14719,11 @@ export type GQLUserInterfacePreferencesResolvers< ParentType, ContextType >; + moderatorSafetySepia?: Resolver< + GQLResolversTypes['Boolean'], + ParentType, + ContextType + >; mrtChartConfigurations?: Resolver< ReadonlyArray, ParentType, diff --git a/server/graphql/modules/user.ts b/server/graphql/modules/user.ts index 45d9feb..edd51e9 100644 --- a/server/graphql/modules/user.ts +++ b/server/graphql/modules/user.ts @@ -79,6 +79,7 @@ const typeDefs = /* GraphQL */ ` type UserInterfacePreferences { moderatorSafetyMuteVideo: Boolean! moderatorSafetyGrayscale: Boolean! + moderatorSafetySepia: Boolean! moderatorSafetyBlurLevel: Int! mrtChartConfigurations: [ManualReviewChartSettings!]! } @@ -87,6 +88,7 @@ const typeDefs = /* GraphQL */ ` moderatorSafetyMuteVideo: Boolean! moderatorSafetyGrayscale: Boolean! moderatorSafetyBlurLevel: Int! + moderatorSafetySepia: Boolean! } input ManualReviewChartConfigurationsInput { diff --git a/server/services/manualReviewToolService/modules/JobDecisioning.ts b/server/services/manualReviewToolService/modules/JobDecisioning.ts index af7edcc..1be10fe 100644 --- a/server/services/manualReviewToolService/modules/JobDecisioning.ts +++ b/server/services/manualReviewToolService/modules/JobDecisioning.ts @@ -413,7 +413,10 @@ export default class JobDecisioning { return { newDecisionStored: logDecisionStatus === 'SUCCESS', - error: match([logDecisionStatus, removeJobStatus] as const) + error: match([logDecisionStatus, removeJobStatus] as readonly [ + 'SUCCESS' | 'ALREADY_LOGGED', + 'SUCCESS' | 'FAILED', + ]) // Case 1, happy path. .with(['SUCCESS', 'SUCCESS'], () => undefined) // Case 2, decision logged but job not deleted. diff --git a/server/services/userManagementService/dbTypes.ts b/server/services/userManagementService/dbTypes.ts index 707d313..97753f3 100644 --- a/server/services/userManagementService/dbTypes.ts +++ b/server/services/userManagementService/dbTypes.ts @@ -27,6 +27,7 @@ export type UserManagementPg = { user_id: string; moderator_safety_mute_video: boolean | null; moderator_safety_grayscale: boolean | null; + moderator_safety_sepia: boolean | null; moderator_safety_blur_level: number | null; mrt_chart_configurations: MrtChartConfig[] | null; }; @@ -45,6 +46,11 @@ export type UserManagementPg = { boolean | undefined, boolean | undefined >; + moderator_safety_sepia: ColumnType< + boolean, + boolean | undefined, + boolean | undefined + >; moderator_safety_blur_level: ColumnType< number, number | undefined, diff --git a/server/services/userManagementService/userManagementService.ts b/server/services/userManagementService/userManagementService.ts index b04b6e2..fa617c8 100644 --- a/server/services/userManagementService/userManagementService.ts +++ b/server/services/userManagementService/userManagementService.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import crypto from 'node:crypto'; import { type Kysely } from 'kysely'; @@ -54,15 +55,17 @@ class UserManagementService { if ( row && - row.moderator_safety_grayscale && - row.moderator_safety_blur_level && - row.moderator_safety_mute_video + row.moderator_safety_grayscale != null && + row.moderator_safety_blur_level != null && + row.moderator_safety_mute_video != null && + row.moderator_safety_sepia != null ) { // If all the user's settings have been set, just return them return { moderatorSafetyGrayscale: row.moderator_safety_grayscale, moderatorSafetyBlurLevel: row.moderator_safety_blur_level, moderatorSafetyMuteVideo: row.moderator_safety_mute_video, + moderatorSafetySepia: row.moderator_safety_sepia, mrtChartConfigurations: row.mrt_chart_configurations ?? [], }; } @@ -73,6 +76,8 @@ class UserManagementService { return { moderatorSafetyGrayscale: row?.moderator_safety_grayscale ?? orgDefaults.moderatorSafetyGrayscale, + moderatorSafetySepia: + row?.moderator_safety_sepia ?? orgDefaults.moderatorSafetySepia, moderatorSafetyBlurLevel: row?.moderator_safety_blur_level ?? orgDefaults.moderatorSafetyBlurLevel, @@ -163,6 +168,7 @@ class UserManagementService { moderatorSafetyMuteVideo: boolean; moderatorSafetyGrayscale: boolean; moderatorSafetyBlurLevel: number; + moderatorSafetySepia: boolean; }; mrtChartConfigurations?: readonly MrtChartConfig[]; }; @@ -176,6 +182,8 @@ class UserManagementService { ? { moderator_safety_grayscale: moderatorSafetySettings.moderatorSafetyGrayscale, + moderator_safety_sepia: + moderatorSafetySettings.moderatorSafetySepia, moderator_safety_blur_level: moderatorSafetySettings.moderatorSafetyBlurLevel, moderator_safety_mute_video: @@ -221,6 +229,7 @@ class UserManagementService { return { moderatorSafetyGrayscale: row.moderator_safety_grayscale, + moderatorSafetySepia: row.moderator_safety_sepia, moderatorSafetyBlurLevel: row.moderator_safety_blur_level, moderatorSafetyMuteVideo: row.moderator_safety_mute_video, }; @@ -231,12 +240,14 @@ class UserManagementService { // If you don't provide these values, they will be set to the default values // configured on the pg table definition moderatorSafetyGrayscale?: boolean; + moderatorSafetySepia?: boolean; moderatorSafetyBlurLevel?: number; moderatorSafetyMuteVideo?: boolean; }) { const { orgId, moderatorSafetyGrayscale, + moderatorSafetySepia, moderatorSafetyBlurLevel, moderatorSafetyMuteVideo, } = opts; @@ -244,6 +255,9 @@ class UserManagementService { ...(moderatorSafetyGrayscale !== undefined ? { moderator_safety_grayscale: moderatorSafetyGrayscale } : {}), + ...(moderatorSafetySepia !== undefined + ? { moderator_safety_sepia: moderatorSafetySepia } + : {}), ...(moderatorSafetyBlurLevel !== undefined ? { moderator_safety_blur_level: moderatorSafetyBlurLevel } : {}), @@ -258,6 +272,7 @@ class UserManagementService { { org_id: orgId, moderator_safety_grayscale: moderatorSafetyGrayscale, + moderator_safety_sepia: moderatorSafetySepia, moderator_safety_blur_level: moderatorSafetyBlurLevel, moderator_safety_mute_video: moderatorSafetyMuteVideo, },