diff --git a/app/soapbox/components/media_gallery.tsx b/app/soapbox/components/media_gallery.tsx index 4f672865b..f899d7ba4 100644 --- a/app/soapbox/components/media_gallery.tsx +++ b/app/soapbox/components/media_gallery.tsx @@ -1,6 +1,5 @@ import classNames from 'clsx'; import React, { useState, useRef, useEffect } from 'react'; -import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import Blurhash from 'soapbox/components/blurhash'; import Icon from 'soapbox/components/icon'; @@ -13,8 +12,6 @@ import { truncateFilename } from 'soapbox/utils/media'; import { isIOS } from '../is_mobile'; import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio'; -import { Button, Text } from './ui'; - import type { Property } from 'csstype'; import type { List as ImmutableList } from 'immutable'; @@ -39,10 +36,6 @@ interface SizeData { width: number, } -const messages = defineMessages({ - toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide' }, -}); - const withinLimits = (aspectRatio: number) => { return aspectRatio >= minimumAspectRatio && aspectRatio <= maximumAspectRatio; }; @@ -276,35 +269,16 @@ interface IMediaGallery { const MediaGallery: React.FC = (props) => { const { media, - sensitive = false, defaultWidth = 0, - onToggleVisibility, onOpenMedia, cacheWidth, compact, height, } = props; - - const intl = useIntl(); - - const settings = useSettings(); - const displayMedia = settings.get('displayMedia') as string | undefined; - - const [visible, setVisible] = useState(props.visible !== undefined ? props.visible : (displayMedia !== 'hide_all' && !sensitive || displayMedia === 'show_all')); const [width, setWidth] = useState(defaultWidth); const node = useRef(null); - const handleOpen: React.MouseEventHandler = (e) => { - e.stopPropagation(); - - if (onToggleVisibility) { - onToggleVisibility(); - } else { - setVisible(!visible); - } - }; - const handleClick = (index: number) => { onOpenMedia(media, index); }; @@ -545,20 +519,13 @@ const MediaGallery: React.FC = (props) => { index={i} size={sizeData.size} displayWidth={sizeData.width} - visible={visible} + visible={!!props.visible} dimensions={sizeData.itemsDimensions[i]} last={i === ATTACHMENT_LIMIT - 1} total={media.size} /> )); - let warning; - - if (sensitive) { - warning = ; - } else { - warning = ; - } useEffect(() => { if (node.current) { @@ -572,60 +539,8 @@ const MediaGallery: React.FC = (props) => { } }, [node.current]); - useEffect(() => { - setVisible(!!props.visible); - }, [props.visible]); - return (
-
- {sensitive && ( - (visible || compact) ? ( - -
-
- ) - )} - - {children} ); diff --git a/app/soapbox/components/status-media.tsx b/app/soapbox/components/status-media.tsx index 0052a9564..4445e67b5 100644 --- a/app/soapbox/components/status-media.tsx +++ b/app/soapbox/components/status-media.tsx @@ -30,7 +30,7 @@ const StatusMedia: React.FC = ({ muted = false, onClick, showMedia = true, - onToggleVisibility = () => {}, + onToggleVisibility = () => { }, }) => { const dispatch = useAppDispatch(); const [mediaWrapperWidth, setMediaWrapperWidth] = useState(undefined); diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 27b1731bd..499460cc3 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -19,7 +19,7 @@ import StatusMedia from './status-media'; import StatusReplyMentions from './status-reply-mentions'; import StatusContent from './status_content'; import ModerationOverlay from './statuses/moderation-overlay'; -import { Card, HStack, Text } from './ui'; +import { Card, HStack, Stack, Text } from './ui'; import type { Map as ImmutableMap } from 'immutable'; import type { @@ -80,7 +80,7 @@ const Status: React.FC = (props) => { const didShowCard = useRef(false); const node = useRef(null); - const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); + const [showMedia, setShowMedia] = useState(status.visibility === 'self' ? false : defaultMediaVisibility(status, displayMedia)); const actualStatus = getActualStatus(status); @@ -90,7 +90,7 @@ const Status: React.FC = (props) => { }, []); useEffect(() => { - setShowMedia(defaultMediaVisibility(status, displayMedia)); + setShowMedia(status.visibility === 'self' ? false : defaultMediaVisibility(status, displayMedia)); }, [status.id]); const handleToggleMediaVisibility = (): void => { @@ -301,6 +301,7 @@ const Status: React.FC = (props) => { const accountAction = props.accountAction || reblogElement; const inReview = status.visibility === 'self'; + const isSensitive = status.sensitive; return ( @@ -351,43 +352,52 @@ const Status: React.FC = (props) => { /> -
- {inReview ? ( - - ) : null} +
+ + {(inReview || isSensitive) ? ( + + ) : null} - {!group && actualStatus.group && ( -
- Posted in {String(actualStatus.getIn(['group', 'title']))} -
- )} + {!group && actualStatus.group && ( +
+ Posted in {String(actualStatus.getIn(['group', 'title']))} +
+ )} - + - + - + - {quote} + {quote} +
{!hideActionBar && (
diff --git a/app/soapbox/components/statuses/__tests__/moderation-overlay.test.tsx b/app/soapbox/components/statuses/__tests__/moderation-overlay.test.tsx index a94923c55..e8a0dc5ef 100644 --- a/app/soapbox/components/statuses/__tests__/moderation-overlay.test.tsx +++ b/app/soapbox/components/statuses/__tests__/moderation-overlay.test.tsx @@ -1,19 +1,111 @@ +import { Map as ImmutableMap } from 'immutable'; import React from 'react'; -import { fireEvent, render, screen } from '../../../jest/test-helpers'; +import { normalizeStatus } from 'soapbox/normalizers'; +import { ReducerStatus } from 'soapbox/reducers/statuses'; + +import { fireEvent, render, rootState, screen } from '../../../jest/test-helpers'; import ModerationOverlay from '../moderation-overlay'; describe('', () => { - it('defaults to enabled', () => { - render(); - expect(screen.getByTestId('moderation-overlay')).toHaveTextContent('Content Under Review'); + let status: ReducerStatus; + + describe('when the Status is marked as sensitive', () => { + beforeEach(() => { + status = normalizeStatus({ sensitive: true }) as ReducerStatus; + }); + + it('displays the "Sensitive content" warning', () => { + render(); + expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Sensitive content'); + }); + + it('can be toggled', () => { + render(); + + fireEvent.click(screen.getByTestId('button')); + expect(screen.getByTestId('sensitive-overlay')).not.toHaveTextContent('Sensitive content'); + expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Hide'); + + fireEvent.click(screen.getByTestId('button')); + expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Sensitive content'); + expect(screen.getByTestId('sensitive-overlay')).not.toHaveTextContent('Hide'); + }); }); - it('can be toggled', () => { - render(); + describe('when the Status is marked as in review', () => { + beforeEach(() => { + status = normalizeStatus({ visibility: 'self', sensitive: false }) as ReducerStatus; + }); - fireEvent.click(screen.getByTestId('button')); - expect(screen.getByTestId('moderation-overlay')).not.toHaveTextContent('Content Under Review'); - expect(screen.getByTestId('moderation-overlay')).toHaveTextContent('Hide'); + it('displays the "Under review" warning', () => { + render(); + expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Content Under Review'); + }); + + it('can be toggled', () => { + render(); + + fireEvent.click(screen.getByTestId('button')); + expect(screen.getByTestId('sensitive-overlay')).not.toHaveTextContent('Content Under Review'); + expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Hide'); + + fireEvent.click(screen.getByTestId('button')); + expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Content Under Review'); + expect(screen.getByTestId('sensitive-overlay')).not.toHaveTextContent('Hide'); + }); + }); + + describe('when the Status is marked as in review and sensitive', () => { + beforeEach(() => { + status = normalizeStatus({ visibility: 'self', sensitive: true }) as ReducerStatus; + }); + + it('displays the "Under review" warning', () => { + render(); + expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Content Under Review'); + }); + + it('can be toggled', () => { + render(); + + fireEvent.click(screen.getByTestId('button')); + expect(screen.getByTestId('sensitive-overlay')).not.toHaveTextContent('Content Under Review'); + expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Hide'); + + fireEvent.click(screen.getByTestId('button')); + expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Content Under Review'); + expect(screen.getByTestId('sensitive-overlay')).not.toHaveTextContent('Hide'); + }); + }); + + describe('when the Status is marked as sensitive and displayMedia set to "show_all"', () => { + let store: any; + + beforeEach(() => { + status = normalizeStatus({ sensitive: true }) as ReducerStatus; + store = rootState + .set('settings', ImmutableMap({ + displayMedia: 'show_all', + })); + }); + + it('displays the "Under review" warning', () => { + render(, undefined, store); + expect(screen.getByTestId('sensitive-overlay')).not.toHaveTextContent('Sensitive content'); + expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Hide'); + }); + + it('can be toggled', () => { + render(, undefined, store); + + fireEvent.click(screen.getByTestId('button')); + expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Sensitive content'); + expect(screen.getByTestId('sensitive-overlay')).not.toHaveTextContent('Hide'); + + fireEvent.click(screen.getByTestId('button')); + expect(screen.getByTestId('sensitive-overlay')).not.toHaveTextContent('Sensitive content'); + expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Hide'); + }); }); }); diff --git a/app/soapbox/components/statuses/moderation-overlay.tsx b/app/soapbox/components/statuses/moderation-overlay.tsx index 6d572eb3a..b85414c2e 100644 --- a/app/soapbox/components/statuses/moderation-overlay.tsx +++ b/app/soapbox/components/statuses/moderation-overlay.tsx @@ -1,78 +1,118 @@ import classNames from 'clsx'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useSoapboxConfig } from 'soapbox/hooks'; +import { useSettings, useSoapboxConfig } from 'soapbox/hooks'; import { Button, HStack, Text } from '../ui'; +import type { Status as StatusEntity } from 'soapbox/types/entities'; + const messages = defineMessages({ - hide: { id: 'moderation_overlay.hide', defaultMessage: 'Hide' }, - title: { id: 'moderation_overlay.title', defaultMessage: 'Content Under Review' }, - subtitle: { id: 'moderation_overlay.subtitle', defaultMessage: 'This Post has been sent to Moderation for review and is only visible to you. If you believe this is an error please contact Support.' }, + hide: { id: 'moderation_overlay.hide', defaultMessage: 'Hide content' }, + sensitiveTitle: { id: 'status.sensitive_warning', defaultMessage: 'Sensitive content' }, + underReviewTitle: { id: 'moderation_overlay.title', defaultMessage: 'Content Under Review' }, + underReviewSubtitle: { id: 'moderation_overlay.subtitle', defaultMessage: 'This Post has been sent to Moderation for review and is only visible to you. If you believe this is an error please contact Support.' }, + sensitiveSubtitle: { id: 'status.sensitive_warning.subtitle', defaultMessage: 'This content may not be suitable for all audiences.' }, contact: { id: 'moderation_overlay.contact', defaultMessage: 'Contact' }, show: { id: 'moderation_overlay.show', defaultMessage: 'Show Content' }, }); -const ModerationOverlay = () => { +interface IModerationOverlay { + status: StatusEntity + onToggleVisibility?(): void + visible?: boolean +} + +const ModerationOverlay = (props: IModerationOverlay) => { + const { onToggleVisibility, status } = props; + const isUnderReview = status.visibility === 'self'; + const isSensitive = status.sensitive; + + const settings = useSettings(); + const displayMedia = settings.get('displayMedia') as string | undefined; + + // under review ovverides displaymedia + const intl = useIntl(); const { links } = useSoapboxConfig(); - const [visible, setVisible] = useState(false); + const [visible, setVisible] = useState( + isUnderReview === true ? false : null + || ( + props.visible !== undefined + ? props.visible + : (displayMedia !== 'hide_all' && !isSensitive || displayMedia === 'show_all') + ), + ); const toggleVisibility = (event: React.MouseEvent) => { event.stopPropagation(); - setVisible((prevValue) => !prevValue); + if (onToggleVisibility) { + onToggleVisibility(); + } else { + setVisible((prevValue) => !prevValue); + } }; + useEffect(() => { + if (typeof props.visible !== 'undefined') { + setVisible(!!props.visible); + } + }, [props.visible]); + return (
{visible ? ( - - )} + {isUnderReview ? ( + <> + {links.get('support') && ( + event.stopPropagation()} + > + + + )} + + ) : null}