Move sensitive filters into new component

This commit is contained in:
Chewbacca 2022-10-20 10:48:41 -04:00
parent 400adfe0c6
commit a639c789a4
8 changed files with 262 additions and 178 deletions

View File

@ -1,6 +1,5 @@
import classNames from 'clsx'; import classNames from 'clsx';
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import Blurhash from 'soapbox/components/blurhash'; import Blurhash from 'soapbox/components/blurhash';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
@ -13,8 +12,6 @@ import { truncateFilename } from 'soapbox/utils/media';
import { isIOS } from '../is_mobile'; import { isIOS } from '../is_mobile';
import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio'; import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio';
import { Button, Text } from './ui';
import type { Property } from 'csstype'; import type { Property } from 'csstype';
import type { List as ImmutableList } from 'immutable'; import type { List as ImmutableList } from 'immutable';
@ -39,10 +36,6 @@ interface SizeData {
width: number, width: number,
} }
const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide' },
});
const withinLimits = (aspectRatio: number) => { const withinLimits = (aspectRatio: number) => {
return aspectRatio >= minimumAspectRatio && aspectRatio <= maximumAspectRatio; return aspectRatio >= minimumAspectRatio && aspectRatio <= maximumAspectRatio;
}; };
@ -276,35 +269,16 @@ interface IMediaGallery {
const MediaGallery: React.FC<IMediaGallery> = (props) => { const MediaGallery: React.FC<IMediaGallery> = (props) => {
const { const {
media, media,
sensitive = false,
defaultWidth = 0, defaultWidth = 0,
onToggleVisibility,
onOpenMedia, onOpenMedia,
cacheWidth, cacheWidth,
compact, compact,
height, height,
} = props; } = props;
const intl = useIntl();
const settings = useSettings();
const displayMedia = settings.get('displayMedia') as string | undefined;
const [visible, setVisible] = useState<boolean>(props.visible !== undefined ? props.visible : (displayMedia !== 'hide_all' && !sensitive || displayMedia === 'show_all'));
const [width, setWidth] = useState<number>(defaultWidth); const [width, setWidth] = useState<number>(defaultWidth);
const node = useRef<HTMLDivElement>(null); const node = useRef<HTMLDivElement>(null);
const handleOpen: React.MouseEventHandler = (e) => {
e.stopPropagation();
if (onToggleVisibility) {
onToggleVisibility();
} else {
setVisible(!visible);
}
};
const handleClick = (index: number) => { const handleClick = (index: number) => {
onOpenMedia(media, index); onOpenMedia(media, index);
}; };
@ -545,20 +519,13 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
index={i} index={i}
size={sizeData.size} size={sizeData.size}
displayWidth={sizeData.width} displayWidth={sizeData.width}
visible={visible} visible={!!props.visible}
dimensions={sizeData.itemsDimensions[i]} dimensions={sizeData.itemsDimensions[i]}
last={i === ATTACHMENT_LIMIT - 1} last={i === ATTACHMENT_LIMIT - 1}
total={media.size} total={media.size}
/> />
)); ));
let warning;
if (sensitive) {
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
} else {
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
}
useEffect(() => { useEffect(() => {
if (node.current) { if (node.current) {
@ -572,60 +539,8 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
} }
}, [node.current]); }, [node.current]);
useEffect(() => {
setVisible(!!props.visible);
}, [props.visible]);
return ( return (
<div className={classNames('media-gallery', { 'media-gallery--compact': compact })} style={sizeData.style} ref={node}> <div className={classNames('media-gallery', { 'media-gallery--compact': compact })} style={sizeData.style} ref={node}>
<div
className={classNames({
'absolute z-40': true,
'inset-0': !visible && !compact,
'left-1 top-1': visible || compact,
})}
>
{sensitive && (
(visible || compact) ? (
<Button
text={intl.formatMessage(messages.toggle_visible)}
icon={visible ? require('@tabler/icons/eye-off.svg') : require('@tabler/icons/eye.svg')}
onClick={handleOpen}
theme='transparent'
size='sm'
/>
) : (
<div
onClick={(e) => e.stopPropagation()}
className={
classNames({
'bg-gray-800/75 cursor-default backdrop-blur-sm rounded-lg w-full h-full border-0 flex items-center justify-center': true,
})
}
>
<div className='text-center w-3/4 mx-auto space-y-4'>
<div className='space-y-1'>
<Text theme='white' weight='semibold'>{warning}</Text>
<Text size='sm'>
<FormattedMessage id='status.sensitive_warning.subtitle' defaultMessage='This content may not be suitable for all audiences.' />
</Text>
</div>
<Button
type='button'
theme='outline'
size='sm'
icon={require('@tabler/icons/eye.svg')}
onClick={handleOpen}
>
<FormattedMessage id='status.sensitive_warning.action' defaultMessage='Show content' />
</Button>
</div>
</div>
)
)}
</div>
{children} {children}
</div> </div>
); );

View File

@ -30,7 +30,7 @@ const StatusMedia: React.FC<IStatusMedia> = ({
muted = false, muted = false,
onClick, onClick,
showMedia = true, showMedia = true,
onToggleVisibility = () => {}, onToggleVisibility = () => { },
}) => { }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [mediaWrapperWidth, setMediaWrapperWidth] = useState<number | undefined>(undefined); const [mediaWrapperWidth, setMediaWrapperWidth] = useState<number | undefined>(undefined);

View File

@ -19,7 +19,7 @@ import StatusMedia from './status-media';
import StatusReplyMentions from './status-reply-mentions'; import StatusReplyMentions from './status-reply-mentions';
import StatusContent from './status_content'; import StatusContent from './status_content';
import ModerationOverlay from './statuses/moderation-overlay'; 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 { Map as ImmutableMap } from 'immutable';
import type { import type {
@ -80,7 +80,7 @@ const Status: React.FC<IStatus> = (props) => {
const didShowCard = useRef(false); const didShowCard = useRef(false);
const node = useRef<HTMLDivElement>(null); const node = useRef<HTMLDivElement>(null);
const [showMedia, setShowMedia] = useState<boolean>(defaultMediaVisibility(status, displayMedia)); const [showMedia, setShowMedia] = useState<boolean>(status.visibility === 'self' ? false : defaultMediaVisibility(status, displayMedia));
const actualStatus = getActualStatus(status); const actualStatus = getActualStatus(status);
@ -90,7 +90,7 @@ const Status: React.FC<IStatus> = (props) => {
}, []); }, []);
useEffect(() => { useEffect(() => {
setShowMedia(defaultMediaVisibility(status, displayMedia)); setShowMedia(status.visibility === 'self' ? false : defaultMediaVisibility(status, displayMedia));
}, [status.id]); }, [status.id]);
const handleToggleMediaVisibility = (): void => { const handleToggleMediaVisibility = (): void => {
@ -301,6 +301,7 @@ const Status: React.FC<IStatus> = (props) => {
const accountAction = props.accountAction || reblogElement; const accountAction = props.accountAction || reblogElement;
const inReview = status.visibility === 'self'; const inReview = status.visibility === 'self';
const isSensitive = status.sensitive;
return ( return (
<HotKeys handlers={handlers} data-testid='status'> <HotKeys handlers={handlers} data-testid='status'>
@ -351,13 +352,21 @@ const Status: React.FC<IStatus> = (props) => {
/> />
</div> </div>
<div <div className='status__content-wrapper'>
className={classNames('status__content-wrapper relative', { <Stack
'min-h-[220px]': inReview, justifyContent='end'
})} className={
classNames('relative', {
'min-h-[220px]': inReview || isSensitive,
})
}
> >
{inReview ? ( {(inReview || isSensitive) ? (
<ModerationOverlay /> <ModerationOverlay
status={status}
visible={showMedia}
onToggleVisibility={handleToggleMediaVisibility}
/>
) : null} ) : null}
{!group && actualStatus.group && ( {!group && actualStatus.group && (
@ -388,6 +397,7 @@ const Status: React.FC<IStatus> = (props) => {
/> />
{quote} {quote}
</Stack>
{!hideActionBar && ( {!hideActionBar && (
<div className='pt-4'> <div className='pt-4'>

View File

@ -1,19 +1,111 @@
import { Map as ImmutableMap } from 'immutable';
import React from 'react'; 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'; import ModerationOverlay from '../moderation-overlay';
describe('<ModerationOverlay />', () => { describe('<ModerationOverlay />', () => {
it('defaults to enabled', () => { let status: ReducerStatus;
render(<ModerationOverlay />);
expect(screen.getByTestId('moderation-overlay')).toHaveTextContent('Content Under Review'); describe('when the Status is marked as sensitive', () => {
beforeEach(() => {
status = normalizeStatus({ sensitive: true }) as ReducerStatus;
});
it('displays the "Sensitive content" warning', () => {
render(<ModerationOverlay status={status} />);
expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Sensitive content');
}); });
it('can be toggled', () => { it('can be toggled', () => {
render(<ModerationOverlay />); render(<ModerationOverlay status={status} />);
fireEvent.click(screen.getByTestId('button')); fireEvent.click(screen.getByTestId('button'));
expect(screen.getByTestId('moderation-overlay')).not.toHaveTextContent('Content Under Review'); expect(screen.getByTestId('sensitive-overlay')).not.toHaveTextContent('Sensitive content');
expect(screen.getByTestId('moderation-overlay')).toHaveTextContent('Hide'); 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');
});
});
describe('when the Status is marked as in review', () => {
beforeEach(() => {
status = normalizeStatus({ visibility: 'self', sensitive: false }) as ReducerStatus;
});
it('displays the "Under review" warning', () => {
render(<ModerationOverlay status={status} />);
expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Content Under Review');
});
it('can be toggled', () => {
render(<ModerationOverlay status={status} />);
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(<ModerationOverlay status={status} />);
expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Content Under Review');
});
it('can be toggled', () => {
render(<ModerationOverlay status={status} />);
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(<ModerationOverlay status={status} />, undefined, store);
expect(screen.getByTestId('sensitive-overlay')).not.toHaveTextContent('Sensitive content');
expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Hide');
});
it('can be toggled', () => {
render(<ModerationOverlay status={status} />, 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');
});
}); });
}); });

View File

@ -1,62 +1,100 @@
import classNames from 'clsx'; import classNames from 'clsx';
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useSoapboxConfig } from 'soapbox/hooks'; import { useSettings, useSoapboxConfig } from 'soapbox/hooks';
import { Button, HStack, Text } from '../ui'; import { Button, HStack, Text } from '../ui';
import type { Status as StatusEntity } from 'soapbox/types/entities';
const messages = defineMessages({ const messages = defineMessages({
hide: { id: 'moderation_overlay.hide', defaultMessage: 'Hide' }, hide: { id: 'moderation_overlay.hide', defaultMessage: 'Hide content' },
title: { id: 'moderation_overlay.title', defaultMessage: 'Content Under Review' }, sensitiveTitle: { id: 'status.sensitive_warning', defaultMessage: 'Sensitive content' },
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.' }, 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' }, contact: { id: 'moderation_overlay.contact', defaultMessage: 'Contact' },
show: { id: 'moderation_overlay.show', defaultMessage: 'Show Content' }, 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 intl = useIntl();
const { links } = useSoapboxConfig(); const { links } = useSoapboxConfig();
const [visible, setVisible] = useState<boolean>(false); const [visible, setVisible] = useState<boolean>(
isUnderReview === true ? false : null
|| (
props.visible !== undefined
? props.visible
: (displayMedia !== 'hide_all' && !isSensitive || displayMedia === 'show_all')
),
);
const toggleVisibility = (event: React.MouseEvent<HTMLButtonElement>) => { const toggleVisibility = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation(); event.stopPropagation();
if (onToggleVisibility) {
onToggleVisibility();
} else {
setVisible((prevValue) => !prevValue); setVisible((prevValue) => !prevValue);
}
}; };
useEffect(() => {
if (typeof props.visible !== 'undefined') {
setVisible(!!props.visible);
}
}, [props.visible]);
return ( return (
<div <div
className={classNames('absolute z-40', { className={classNames('absolute z-40', {
'cursor-default backdrop-blur-lg rounded-lg w-full h-full border-0 flex justify-center items-center': !visible, 'cursor-default backdrop-blur-lg rounded-lg w-full h-full border-0 flex justify-center items-center': !visible,
'bg-gray-800/75 inset-0': !visible, 'bg-gray-800/75 inset-0': !visible,
'top-1 left-1': visible, 'bottom-1 right-1': visible,
})} })}
data-testid='moderation-overlay' data-testid='sensitive-overlay'
> >
{visible ? ( {visible ? (
<Button <Button
text={intl.formatMessage(messages.hide)} text={intl.formatMessage(messages.hide)}
icon={require('@tabler/icons/eye-off.svg')} icon={require('@tabler/icons/eye-off.svg')}
onClick={toggleVisibility} onClick={toggleVisibility}
theme='transparent' theme='primary'
size='sm' size='sm'
/> />
) : ( ) : (
<div className='text-center w-3/4 mx-auto space-y-4'> <div className='text-center w-3/4 mx-auto space-y-4'>
<div className='space-y-1'> <div className='space-y-1'>
<Text theme='white' weight='semibold'> <Text theme='white' weight='semibold'>
{intl.formatMessage(messages.title)} {intl.formatMessage(isUnderReview ? messages.underReviewTitle : messages.sensitiveTitle)}
</Text> </Text>
<Text theme='white' size='sm' weight='medium'> <Text theme='white' size='sm' weight='medium'>
{intl.formatMessage(messages.subtitle)} {intl.formatMessage(isUnderReview ? messages.underReviewSubtitle : messages.sensitiveSubtitle)}
</Text> </Text>
</div> </div>
<HStack alignItems='center' justifyContent='center' space={2}> <HStack alignItems='center' justifyContent='center' space={2}>
{isUnderReview ? (
<>
{links.get('support') && ( {links.get('support') && (
<a <a
href={links.get('support')} href={links.get('support')}
@ -73,6 +111,8 @@ const ModerationOverlay = () => {
</Button> </Button>
</a> </a>
)} )}
</>
) : null}
<Button <Button
type='button' type='button'

View File

@ -15,6 +15,7 @@ const spaces = {
const justifyContentOptions = { const justifyContentOptions = {
center: 'justify-center', center: 'justify-center',
end: 'justify-end',
}; };
const alignItemsOptions = { const alignItemsOptions = {
@ -27,7 +28,7 @@ interface IStack extends React.HTMLAttributes<HTMLDivElement> {
/** Horizontal alignment of children. */ /** Horizontal alignment of children. */
alignItems?: 'center' alignItems?: 'center'
/** Vertical alignment of children. */ /** Vertical alignment of children. */
justifyContent?: 'center' justifyContent?: keyof typeof justifyContentOptions
/** Extra class names on the <div> element. */ /** Extra class names on the <div> element. */
className?: string className?: string
/** Whether to let the flexbox grow. */ /** Whether to let the flexbox grow. */

View File

@ -1,3 +1,4 @@
import classNames from 'clsx';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { FormattedDate, FormattedMessage, useIntl } from 'react-intl'; import { FormattedDate, FormattedMessage, useIntl } from 'react-intl';
@ -5,7 +6,8 @@ import Icon from 'soapbox/components/icon';
import StatusMedia from 'soapbox/components/status-media'; import StatusMedia from 'soapbox/components/status-media';
import StatusReplyMentions from 'soapbox/components/status-reply-mentions'; import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
import StatusContent from 'soapbox/components/status_content'; import StatusContent from 'soapbox/components/status_content';
import { HStack, Text } from 'soapbox/components/ui'; import ModerationOverlay from 'soapbox/components/statuses/moderation-overlay';
import { HStack, Stack, Text } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container'; import AccountContainer from 'soapbox/containers/account_container';
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
import { getActualStatus } from 'soapbox/utils/status'; import { getActualStatus } from 'soapbox/utils/status';
@ -48,6 +50,9 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
const { account } = actualStatus; const { account } = actualStatus;
if (!account || typeof account !== 'object') return null; if (!account || typeof account !== 'object') return null;
const inReview = actualStatus.visibility === 'self';
const isSensitive = actualStatus.sensitive;
let statusTypeIcon = null; let statusTypeIcon = null;
let quote; let quote;
@ -85,6 +90,22 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
<StatusReplyMentions status={actualStatus} /> <StatusReplyMentions status={actualStatus} />
<Stack
justifyContent='end'
className={
classNames('relative', {
'min-h-[220px]': inReview || isSensitive,
})
}
>
{(inReview || isSensitive) ? (
<ModerationOverlay
status={status}
visible={showMedia}
onToggleVisibility={onToggleMediaVisibility}
/>
) : null}
<StatusContent <StatusContent
status={actualStatus} status={actualStatus}
expanded={!actualStatus.hidden} expanded={!actualStatus.hidden}
@ -98,6 +119,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
/> />
{quote} {quote}
</Stack>
<HStack justifyContent='between' alignItems='center' className='py-2' wrap> <HStack justifyContent='between' alignItems='center' className='py-2' wrap>
<StatusInteractionBar status={actualStatus} /> <StatusInteractionBar status={actualStatus} />

View File

@ -156,7 +156,7 @@ const Thread: React.FC<IThread> = (props) => {
}; };
}); });
const [showMedia, setShowMedia] = useState<boolean>(defaultMediaVisibility(status, displayMedia)); const [showMedia, setShowMedia] = useState<boolean>(status?.visibility === 'self' ? false : defaultMediaVisibility(status, displayMedia));
const [isLoaded, setIsLoaded] = useState<boolean>(!!status); const [isLoaded, setIsLoaded] = useState<boolean>(!!status);
const [next, setNext] = useState<string>(); const [next, setNext] = useState<string>();
@ -165,7 +165,7 @@ const Thread: React.FC<IThread> = (props) => {
const scroller = useRef<VirtuosoHandle>(null); const scroller = useRef<VirtuosoHandle>(null);
/** Fetch the status (and context) from the API. */ /** Fetch the status (and context) from the API. */
const fetchData = async() => { const fetchData = async () => {
const { params } = props; const { params } = props;
const { statusId } = params; const { statusId } = params;
const { next } = await dispatch(fetchStatusWithContext(statusId)); const { next } = await dispatch(fetchStatusWithContext(statusId));
@ -393,7 +393,7 @@ const Thread: React.FC<IThread> = (props) => {
// Reset media visibility if status changes. // Reset media visibility if status changes.
useEffect(() => { useEffect(() => {
setShowMedia(defaultMediaVisibility(status, displayMedia)); setShowMedia(status?.visibility === 'self' ? false : defaultMediaVisibility(status, displayMedia));
}, [status?.id]); }, [status?.id]);
// Scroll focused status into view when thread updates. // Scroll focused status into view when thread updates.
@ -414,7 +414,7 @@ const Thread: React.FC<IThread> = (props) => {
if (next && status) { if (next && status) {
dispatch(fetchNext(status.id, next)).then(({ next }) => { dispatch(fetchNext(status.id, next)).then(({ next }) => {
setNext(next); setNext(next);
}).catch(() => {}); }).catch(() => { });
} }
}, 300, { leading: true }), [next, status]); }, 300, { leading: true }), [next, status]);
@ -471,7 +471,11 @@ const Thread: React.FC<IThread> = (props) => {
aria-label={textForScreenReader(intl, status)} aria-label={textForScreenReader(intl, status)}
> >
{inReview ? ( {inReview ? (
<ModerationOverlay /> <ModerationOverlay
status={status}
visible={showMedia}
onToggleVisibility={handleToggleMediaVisibility}
/>
) : null} ) : null}
<DetailedStatus <DetailedStatus