Move sensitive filters into new component
This commit is contained in:
parent
400adfe0c6
commit
a639c789a4
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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'>
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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. */
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue