diff --git a/packages/pl-fe/src/actions/filters.ts b/packages/pl-fe/src/actions/filters.ts index 4b81f6f06..8399e60bf 100644 --- a/packages/pl-fe/src/actions/filters.ts +++ b/packages/pl-fe/src/actions/filters.ts @@ -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, hide: boolean, keywords_attributes: FilterKeywords) => +const createFilter = (title: string, expires_in: number | undefined, context: Array, 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, hide: boolean, keywords_attributes: FilterKeywords) => +const updateFilter = (filterId: string, title: string, expires_in: number | undefined, context: Array, 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 => { diff --git a/packages/pl-fe/src/components/attachment-thumbs.tsx b/packages/pl-fe/src/components/attachment-thumbs.tsx index 69d6f434f..a0b919fd1 100644 --- a/packages/pl-fe/src/components/attachment-thumbs.tsx +++ b/packages/pl-fe/src/components/attachment-thumbs.tsx @@ -10,7 +10,7 @@ import type { MediaAttachment } from 'pl-api'; import type { Status } from 'pl-fe/normalizers/status'; interface IAttachmentThumbs { - status: Pick; + status: Pick; onClick?(): void; } @@ -21,7 +21,7 @@ const AttachmentThumbs = ({ status, onClick }: IAttachmentThumbs) => { const fallback =
; const onOpenMedia = (media: Array, index: number) => openModal('MEDIA', { media, index }); - const visible = useMediaVisible(status, displayMedia); + const [visible] = useMediaVisible(status, displayMedia); return (
diff --git a/packages/pl-fe/src/components/status-media.tsx b/packages/pl-fe/src/components/status-media.tsx index 43fd8ec70..6d18322f2 100644 --- a/packages/pl-fe/src/components/status-media.tsx +++ b/packages/pl-fe/src/components/status-media.tsx @@ -14,7 +14,7 @@ import type { Status } from 'pl-fe/normalizers/status'; interface IStatusMedia { /** Status entity to render media for. */ - status: Pick; + status: Pick; /** Whether to display compact media. */ muted?: boolean; /** Callback when compact media is clicked. */ @@ -30,7 +30,7 @@ const StatusMedia: React.FC = ({ 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]; diff --git a/packages/pl-fe/src/components/status.tsx b/packages/pl-fe/src/components/status.tsx index 1df051a5d..cdf8b89f3 100644 --- a/packages/pl-fe/src/components/status.tsx +++ b/packages/pl-fe/src/components/status.tsx @@ -97,7 +97,8 @@ const Status: React.FC = (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 = (props) => {
- : {status.filtered.join(', ')}. + : {filterResults.map(({ filter }) => filter.title).join(', ')}. {' '}
diff --git a/packages/pl-fe/src/features/compose/components/reply-indicator.tsx b/packages/pl-fe/src/features/compose/components/reply-indicator.tsx index 9f9f3cbff..5b28fc604 100644 --- a/packages/pl-fe/src/features/compose/components/reply-indicator.tsx +++ b/packages/pl-fe/src/features/compose/components/reply-indicator.tsx @@ -12,7 +12,7 @@ import type { Status } from 'pl-fe/normalizers/status'; interface IReplyIndicator { className?: string; - status?: Pick; + status?: Pick; onCancel?: () => void; hideActions: boolean; } diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index fbe55c3b7..1d3e8ef15 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -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 “{title}”", "status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.", "status.share": "Share", "status.show_filter_reason": "Show anyway", diff --git a/packages/pl-fe/src/modals/compare-history-modal.tsx b/packages/pl-fe/src/modals/compare-history-modal.tsx index c0601459e..00d8abaec 100644 --- a/packages/pl-fe/src/modals/compare-history-modal.tsx +++ b/packages/pl-fe/src/modals/compare-history-modal.tsx @@ -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 = )} {version.media_attachments.length > 0 && ( - + )} diff --git a/packages/pl-fe/src/normalizers/status.ts b/packages/pl-fe/src/normalizers/status.ts index 119c827b1..9b1ec728a 100644 --- a/packages/pl-fe/src/normalizers/status.ts +++ b/packages/pl-fe/src/normalizers/status.ts @@ -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, diff --git a/packages/pl-fe/src/pages/settings/edit-filter.tsx b/packages/pl-fe/src/pages/settings/edit-filter.tsx index d590903f0..f643a1b2b 100644 --- a/packages/pl-fe/src/pages/settings/edit-filter.tsx +++ b/packages/pl-fe/src/pages/settings/edit-filter.tsx @@ -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 = ({ 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('warn'); const [keywords, setKeywords] = useState([{ keyword: '', whole_word: false }]); const expirations = useMemo(() => ({ @@ -145,8 +151,8 @@ const EditFilterPage: React.FC = ({ 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 = ({ 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 = ({ params }) => { - - setHide(target.checked)} - /> - + {features.filtersV2BlurAction ? ( + + + + ) : ( + + setFilterAction(target.checked ? 'hide' : 'warn')} + /> + + )} {features.filtersV2 && keywordsField} diff --git a/packages/pl-fe/src/pages/settings/filters.tsx b/packages/pl-fe/src/pages/settings/filters.tsx index 19616457d..cd9668e26 100644 --- a/packages/pl-fe/src/pages/settings/filters.tsx +++ b/packages/pl-fe/src/pages/settings/filters.tsx @@ -93,7 +93,9 @@ const FiltersPage = () => { {filtersV2 ? ( filter.filter_action === 'hide' ? : - + filter.filter_action === 'blur' ? + : + ) : (filter.filter_action === 'hide' ? : )} diff --git a/packages/pl-fe/src/selectors/index.ts b/packages/pl-fe/src/selectors/index.ts index 0c5e32988..4f0e921a8 100644 --- a/packages/pl-fe/src/selectors/index.ts +++ b/packages/pl-fe/src/selectors/index.ts @@ -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) => { }; const checkFiltered = (index: string, filters: Array) => - filters.reduce((result: Array, filter) => - result.concat(filter.keywords.reduce((result: Array, keyword) => { + filters.reduce((result: Array, filter) => + result.concat(filter.keywords.reduce((result: Array, keyword) => { let expr = escapeRegExp(keyword.keyword); if (keyword.whole_word) { @@ -116,7 +116,7 @@ const checkFiltered = (index: string, filters: Array) => 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; }, [])), []);