pl-fe: support filters blur action

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-08-13 09:52:47 +02:00
parent 38d155d7a3
commit 122a2a42c1
12 changed files with 87 additions and 45 deletions

View File

@ -10,7 +10,7 @@ import type { MediaAttachment } from 'pl-api';
import type { Status } from 'pl-fe/normalizers/status';
interface IAttachmentThumbs {
status: Pick<Status, 'media_attachments' | 'sensitive'>;
status: Pick<Status, 'filtered' | 'media_attachments' | 'sensitive'>;
onClick?(): void;
}
@ -21,7 +21,7 @@ const AttachmentThumbs = ({ status, onClick }: IAttachmentThumbs) => {
const fallback = <div className='media-gallery--compact' />;
const onOpenMedia = (media: Array<MediaAttachment>, index: number) => openModal('MEDIA', { media, index });
const visible = useMediaVisible(status, displayMedia);
const [visible] = useMediaVisible(status, displayMedia);
return (
<div className='relative'>

View File

@ -14,7 +14,7 @@ import type { Status } from 'pl-fe/normalizers/status';
interface IStatusMedia {
/** Status entity to render media for. */
status: Pick<Status, 'id' | 'account' | 'card' | 'expectsCard' | 'media_attachments' | 'quote_id' | 'sensitive' | 'visibility'>;
status: Pick<Status, 'id' | 'account' | 'card' | 'expectsCard' | 'filtered' | 'media_attachments' | 'quote_id' | 'sensitive' | 'visibility'>;
/** Whether to display compact media. */
muted?: boolean;
/** Callback when compact media is clicked. */
@ -30,7 +30,7 @@ const StatusMedia: React.FC<IStatusMedia> = ({
const { openModal } = useModalsStore();
const { displayMedia } = useSettings();
const visible = useMediaVisible(status, displayMedia);
const [visible] = useMediaVisible(status, displayMedia);
const size = status.media_attachments.length;
const firstAttachment = status.media_attachments[0];

View File

@ -97,7 +97,8 @@ const Status: React.FC<IStatus> = (props) => {
const statusUrl = `/@${actualStatus.account.acct}/posts/${actualStatus.id}`;
const group = actualStatus.group;
const filtered = (status.filtered?.length || actualStatus.filtered?.length) > 0;
const filterResults = useMemo(() => [...status.filtered, ...actualStatus.filtered].filter(({ filter }) => filter.filter_action === 'warn'), [status.filtered, actualStatus.filtered]);
const filtered = filterResults.length > 0;
// Track height changes we know about to compensate scrolling.
useEffect(() => {
@ -336,7 +337,7 @@ const Status: React.FC<IStatus> = (props) => {
<HotKeys handlers={minHandlers} attachRef={node}>
<div className={clsx('status__wrapper text-center', { focusable })} tabIndex={focusable ? 0 : undefined} ref={node}>
<Text theme='muted'>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {status.filtered.join(', ')}.
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {filterResults.map(({ filter }) => filter.title).join(', ')}.
{' '}
<button className='text-primary-600 hover:underline dark:text-accent-blue' onClick={handleUnfilter}>
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />

View File

@ -1,5 +1,5 @@
import clsx from 'clsx';
import React from 'react';
import React, { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Button from 'pl-fe/components/ui/button';
@ -8,23 +8,30 @@ import Text from 'pl-fe/components/ui/text';
import { useSettings } from 'pl-fe/hooks/use-settings';
import { useStatusMetaStore } from 'pl-fe/stores/status-meta';
import type { FilterResult } from 'pl-api';
import type { Status } from 'pl-fe/normalizers/status';
const useMediaVisible = (status: Pick<Status, 'media_attachments' | 'sensitive'> & { id?: string }, displayMedia: 'default' | 'show_all' | 'hide_all') => {
let visible = !status.sensitive;
const useMediaVisible = (status: Pick<Status, 'filtered' | 'media_attachments' | 'sensitive'> & { id?: string }, displayMedia: 'default' | 'show_all' | 'hide_all'): [boolean, Array<FilterResult>] => {
const statusesMeta = useStatusMetaStore().statuses;
const mediaVisible = status.id ? statusesMeta[status.id]?.mediaVisible : undefined;
if (mediaVisible !== undefined) visible = mediaVisible;
else if (displayMedia === 'show_all') visible = true;
else if (displayMedia === 'hide_all' && status.media_attachments.length) visible = false;
return useMemo(() => {
let visible = !status.sensitive;
return visible;
const filterResults = status.filtered.filter(({ filter }) => filter.filter_action === 'blur');
if (filterResults.length) return [mediaVisible !== undefined ? mediaVisible : false, filterResults];
if (mediaVisible !== undefined) visible = mediaVisible;
else if (displayMedia === 'show_all') visible = true;
else if (displayMedia === 'hide_all' && status.media_attachments.length) visible = false;
return [visible, []];
}, [status.sensitive, status.filtered, mediaVisible]);
};
const useShowOverlay = (status: Pick<Status, 'id' | 'media_attachments' | 'sensitive'>, displayMedia: 'default' | 'show_all' | 'hide_all') => {
const visible = useMediaVisible(status, displayMedia);
const useShowOverlay = (status: Pick<Status, 'id' | 'filtered' | 'media_attachments' | 'sensitive'>, displayMedia: 'default' | 'show_all' | 'hide_all') => {
const [visible] = useMediaVisible(status, displayMedia);
const showHideButton = status.sensitive || (status.media_attachments.length && displayMedia === 'hide_all');
@ -40,10 +47,11 @@ const messages = defineMessages({
sensitiveTitle: { id: 'status.sensitive_warning', defaultMessage: 'Sensitive content' },
sensitiveSubtitle: { id: 'status.sensitive_warning.subtitle', defaultMessage: 'This content may not be suitable for all audiences.' },
show: { id: 'moderation_overlay.show', defaultMessage: 'Show content' },
matchesFilter: { id: 'status.sensitive_warning.matches_filter', defaultMessage: 'Matches filter “<span>{title}</span>”' },
});
interface ISensitiveContentOverlay {
status: Pick<Status, 'id' | 'sensitive' | 'media_attachments'>;
status: Pick<Status, 'id' | 'filtered' | 'sensitive' | 'media_attachments'>;
}
const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveContentOverlay>((props, ref) => {
@ -52,7 +60,9 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
const intl = useIntl();
const { displayMedia } = useSettings();
const visible = useMediaVisible(status, displayMedia);
const [visible, filters] = useMediaVisible(status, displayMedia);
const matchedFilters = useMemo(() => filters.map(({ filter }) => filter.title), [filters]);
const { hideStatusMedia, revealStatusMedia } = useStatusMetaStore();
@ -91,7 +101,10 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
</Text>
<Text theme='white' size='sm' weight='medium'>
{intl.formatMessage(messages.sensitiveSubtitle)}
{filters.length ? intl.formatMessage(messages.matchesFilter, {
title: matchedFilters.join(', '),
span: (chunks) => <span className='filter-name'>{chunks}</span>,
}) : intl.formatMessage(messages.sensitiveSubtitle)}
</Text>
</div>