pl-fe: support filters blur action
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -35,12 +35,12 @@ const fetchFilter = (filterId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
getClient(getState).filtering.getFilter(filterId);
|
||||
|
||||
const createFilter = (title: string, expires_in: number | undefined, context: Array<FilterContext>, hide: boolean, keywords_attributes: FilterKeywords) =>
|
||||
const createFilter = (title: string, expires_in: number | undefined, context: Array<FilterContext>, filter_action: Filter['filter_action'], keywords_attributes: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
getClient(getState).filtering.createFilter({
|
||||
title,
|
||||
context,
|
||||
filter_action: hide ? 'hide' : 'warn',
|
||||
filter_action,
|
||||
expires_in,
|
||||
keywords_attributes,
|
||||
}).then(response => {
|
||||
@ -49,12 +49,12 @@ const createFilter = (title: string, expires_in: number | undefined, context: Ar
|
||||
return response;
|
||||
});
|
||||
|
||||
const updateFilter = (filterId: string, title: string, expires_in: number | undefined, context: Array<FilterContext>, hide: boolean, keywords_attributes: FilterKeywords) =>
|
||||
const updateFilter = (filterId: string, title: string, expires_in: number | undefined, context: Array<FilterContext>, filter_action: Filter['filter_action'], keywords_attributes: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
getClient(getState).filtering.updateFilter(filterId, {
|
||||
title,
|
||||
context,
|
||||
filter_action: hide ? 'hide' : 'warn',
|
||||
filter_action,
|
||||
expires_in,
|
||||
keywords_attributes,
|
||||
}).then(response => {
|
||||
|
||||
@ -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'>
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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' />
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import type { Status } from 'pl-fe/normalizers/status';
|
||||
|
||||
interface IReplyIndicator {
|
||||
className?: string;
|
||||
status?: Pick<Status, 'account_id' | 'content' | 'created_at' | 'emojis' | 'media_attachments' | 'mentions' | 'search_index' | 'sensitive' | 'spoiler_text' | 'quote_id'>;
|
||||
status?: Pick<Status, 'account_id' | 'content' | 'created_at' | 'emojis' | 'filtered' | 'media_attachments' | 'mentions' | 'search_index' | 'sensitive' | 'spoiler_text' | 'quote_id'>;
|
||||
onCancel?: () => void;
|
||||
hideActions: boolean;
|
||||
}
|
||||
|
||||
@ -428,6 +428,11 @@
|
||||
"column.filters.expiration.86400": "1 day",
|
||||
"column.filters.expiration.never": "Never",
|
||||
"column.filters.expires": "Expire after",
|
||||
"column.filters.filter_action_blur": "Hide media with a warning",
|
||||
"column.filters.filter_action_header": "Filter action",
|
||||
"column.filters.filter_action_hide": "Hide completely",
|
||||
"column.filters.filter_action_hint": "Choose which action to perform when a post matches the filter",
|
||||
"column.filters.filter_action_warn": "Hide with a warning",
|
||||
"column.filters.hide_header": "Hide completely",
|
||||
"column.filters.hide_hint": "Completely hide the filtered content, instead of showing a warning",
|
||||
"column.filters.home_timeline": "Home timeline",
|
||||
@ -879,6 +884,7 @@
|
||||
"filters.context_header": "Filter contexts",
|
||||
"filters.context_hint": "One or multiple contexts where the filter should apply",
|
||||
"filters.create_filter": "Create filter",
|
||||
"filters.filters_list_blur": "Hide media with a warning",
|
||||
"filters.filters_list_context_label": "Filter contexts:",
|
||||
"filters.filters_list_drop": "Drop",
|
||||
"filters.filters_list_expired": "Expired",
|
||||
@ -1683,6 +1689,7 @@
|
||||
"status.reply_all": "Reply to thread",
|
||||
"status.report": "Report @{name}",
|
||||
"status.sensitive_warning": "Sensitive content",
|
||||
"status.sensitive_warning.matches_filter": "Matches filter “<span>{title}</span>”",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Share",
|
||||
"status.show_filter_reason": "Show anyway",
|
||||
|
||||
@ -13,6 +13,7 @@ import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useStatusHistory } from 'pl-fe/queries/statuses/use-status-history';
|
||||
|
||||
import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root';
|
||||
import type { Status } from 'pl-fe/normalizers/status';
|
||||
|
||||
interface CompareHistoryModalProps {
|
||||
statusId: string;
|
||||
@ -75,7 +76,7 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
||||
)}
|
||||
|
||||
{version.media_attachments.length > 0 && (
|
||||
<AttachmentThumbs status={version} />
|
||||
<AttachmentThumbs status={version as Status} />
|
||||
)}
|
||||
|
||||
<Text align='right' tag='span' theme='muted' size='sm'>
|
||||
|
||||
@ -121,7 +121,6 @@ const normalizeStatus = (status: BaseStatus & {
|
||||
account: normalizeAccount(status.account),
|
||||
accounts: status.accounts?.map(normalizeAccount),
|
||||
mentions,
|
||||
filtered: status.filtered?.map(result => result.filter.title),
|
||||
event,
|
||||
group,
|
||||
media_attachments,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Filter, type FilterContext } from 'pl-api';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
@ -12,6 +13,7 @@ import FormActions from 'pl-fe/components/ui/form-actions';
|
||||
import FormGroup from 'pl-fe/components/ui/form-group';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Input from 'pl-fe/components/ui/input';
|
||||
import Select from 'pl-fe/components/ui/select';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Streamfield from 'pl-fe/components/ui/streamfield';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
@ -21,7 +23,6 @@ import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
import type { FilterContext } from 'pl-api';
|
||||
import type { StreamfieldComponent } from 'pl-fe/components/ui/streamfield';
|
||||
|
||||
interface IFilterField {
|
||||
@ -50,6 +51,11 @@ const messages = defineMessages({
|
||||
drop_hint: { id: 'column.filters.drop_hint', defaultMessage: 'Filtered posts will disappear irreversibly, even if filter is later removed' },
|
||||
hide_header: { id: 'column.filters.hide_header', defaultMessage: 'Hide completely' },
|
||||
hide_hint: { id: 'column.filters.hide_hint', defaultMessage: 'Completely hide the filtered content, instead of showing a warning' },
|
||||
filter_action_header: { id: 'column.filters.filter_action_header', defaultMessage: 'Filter action' },
|
||||
filter_action_hint: { id: 'column.filters.filter_action_hint', defaultMessage: 'Choose which action to perform when a post matches the filter' },
|
||||
filter_action_warn: { id: 'column.filters.filter_action_warn', defaultMessage: 'Hide with a warning' },
|
||||
filter_action_blur: { id: 'column.filters.filter_action_blur', defaultMessage: 'Hide media with a warning' },
|
||||
filter_action_hide: { id: 'column.filters.filter_action_hide', defaultMessage: 'Hide completely' },
|
||||
add_new: { id: 'column.filters.add_new', defaultMessage: 'Add new filter' },
|
||||
edit: { id: 'column.filters.edit', defaultMessage: 'Edit filter' },
|
||||
create_error: { id: 'column.filters.create_error', defaultMessage: 'Error adding filter' },
|
||||
@ -107,7 +113,7 @@ const EditFilterPage: React.FC<IEditFilter> = ({ params }) => {
|
||||
const [notifications, setNotifications] = useState(false);
|
||||
const [conversations, setConversations] = useState(false);
|
||||
const [accounts, setAccounts] = useState(false);
|
||||
const [hide, setHide] = useState(false);
|
||||
const [filterAction, setFilterAction] = useState<Filter['filter_action']>('warn');
|
||||
const [keywords, setKeywords] = useState<IFilterField[]>([{ keyword: '', whole_word: false }]);
|
||||
|
||||
const expirations = useMemo(() => ({
|
||||
@ -145,8 +151,8 @@ const EditFilterPage: React.FC<IEditFilter> = ({ params }) => {
|
||||
}
|
||||
|
||||
dispatch(params.id
|
||||
? updateFilter(params.id, title, expiresIn, context, hide, keywords)
|
||||
: createFilter(title, expiresIn, context, hide, keywords)).then(() => {
|
||||
? updateFilter(params.id, title, expiresIn, context, filterAction, keywords)
|
||||
: createFilter(title, expiresIn, context, filterAction, keywords)).then(() => {
|
||||
history.push('/filters');
|
||||
}).catch(() => {
|
||||
toast.error(intl.formatMessage(messages.create_error));
|
||||
@ -172,7 +178,7 @@ const EditFilterPage: React.FC<IEditFilter> = ({ params }) => {
|
||||
setNotifications(filter.context.includes('notifications'));
|
||||
setConversations(filter.context.includes('thread'));
|
||||
setAccounts(filter.context.includes('account'));
|
||||
setHide(filter.filter_action === 'hide');
|
||||
setFilterAction(filter.filter_action);
|
||||
setKeywords(filter.keywords);
|
||||
} else {
|
||||
setNotFound(true);
|
||||
@ -265,15 +271,28 @@ const EditFilterPage: React.FC<IEditFilter> = ({ params }) => {
|
||||
</List>
|
||||
|
||||
<List>
|
||||
<ListItem
|
||||
label={intl.formatMessage(features.filtersV2 ? messages.hide_header : messages.drop_header)}
|
||||
hint={intl.formatMessage(features.filtersV2 ? messages.hide_hint : messages.drop_hint)}
|
||||
>
|
||||
<Toggle
|
||||
checked={hide}
|
||||
onChange={({ target }) => setHide(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
{features.filtersV2BlurAction ? (
|
||||
<ListItem
|
||||
label={intl.formatMessage(messages.filter_action_header)}
|
||||
hint={intl.formatMessage(messages.filter_action_hint)}
|
||||
>
|
||||
<Select value={filterAction} onChange={({ target }) => setFilterAction(target.value as Filter['filter_action'])}>
|
||||
<option value='warn'>{intl.formatMessage(messages.filter_action_warn)}</option>
|
||||
<option value='hide'>{intl.formatMessage(messages.filter_action_hide)}</option>
|
||||
<option value='blur'>{intl.formatMessage(messages.filter_action_blur)}</option>
|
||||
</Select>
|
||||
</ListItem>
|
||||
) : (
|
||||
<ListItem
|
||||
label={intl.formatMessage(features.filtersV2 ? messages.hide_header : messages.drop_header)}
|
||||
hint={intl.formatMessage(features.filtersV2 ? messages.hide_hint : messages.drop_hint)}
|
||||
>
|
||||
<Toggle
|
||||
checked={filterAction === 'hide'}
|
||||
onChange={({ target }) => setFilterAction(target.checked ? 'hide' : 'warn')}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
|
||||
{features.filtersV2 && keywordsField}
|
||||
|
||||
@ -93,7 +93,9 @@ const FiltersPage = () => {
|
||||
{filtersV2 ? (
|
||||
filter.filter_action === 'hide' ?
|
||||
<FormattedMessage id='filters.filters_list_hide_completely' defaultMessage='Hide content' /> :
|
||||
<FormattedMessage id='filters.filters_list_warn' defaultMessage='Display warning' />
|
||||
filter.filter_action === 'blur' ?
|
||||
<FormattedMessage id='filters.filters_list_blur' defaultMessage='Hide media with a warning' /> :
|
||||
<FormattedMessage id='filters.filters_list_warn' defaultMessage='Display warning' />
|
||||
) : (filter.filter_action === 'hide' ?
|
||||
<FormattedMessage id='filters.filters_list_drop' defaultMessage='Drop' /> :
|
||||
<FormattedMessage id='filters.filters_list_hide' defaultMessage='Hide' />)}
|
||||
|
||||
@ -8,7 +8,7 @@ import { validId } from 'pl-fe/utils/auth';
|
||||
import ConfigDB from 'pl-fe/utils/config-db';
|
||||
import { shouldFilter } from 'pl-fe/utils/timelines';
|
||||
|
||||
import type { Filter, NotificationGroup, Relationship } from 'pl-api';
|
||||
import type { Filter, FilterResult, NotificationGroup, Relationship } from 'pl-api';
|
||||
import type { EntityStore } from 'pl-fe/entity-store/types';
|
||||
import type { Account } from 'pl-fe/normalizers/account';
|
||||
import type { Group } from 'pl-fe/normalizers/group';
|
||||
@ -100,8 +100,8 @@ const regexFromFilters = (filters: Array<Filter>) => {
|
||||
};
|
||||
|
||||
const checkFiltered = (index: string, filters: Array<Filter>) =>
|
||||
filters.reduce((result: Array<string>, filter) =>
|
||||
result.concat(filter.keywords.reduce((result: Array<string>, keyword) => {
|
||||
filters.reduce((result: Array<FilterResult>, filter) =>
|
||||
result.concat(filter.keywords.reduce((result: Array<FilterResult>, keyword) => {
|
||||
let expr = escapeRegExp(keyword.keyword);
|
||||
|
||||
if (keyword.whole_word) {
|
||||
@ -116,7 +116,7 @@ const checkFiltered = (index: string, filters: Array<Filter>) =>
|
||||
|
||||
const regex = new RegExp(expr);
|
||||
|
||||
if (regex.test(index)) return result.concat(filter.title);
|
||||
if (regex.test(index)) return result.concat({ filter, keyword_matches: null, status_matches: null });
|
||||
return result;
|
||||
}, [])), []);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user