From 4200fa2df4ed4607abb332599222d0c04ae85db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 3 Mar 2023 22:40:39 +0100 Subject: [PATCH] filters v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- README.md | 8 +-- app/assets/icons/COPYING.md | 2 +- app/soapbox/actions/filters.ts | 70 +++++++++++++++++------ app/soapbox/actions/statuses.ts | 9 +++ app/soapbox/components/status.tsx | 14 ++++- app/soapbox/locales/pl.json | 2 +- app/soapbox/normalizers/filter-keyword.ts | 18 ++++++ app/soapbox/normalizers/filter-result.ts | 22 +++++++ app/soapbox/normalizers/filter-status.ts | 17 ++++++ app/soapbox/normalizers/filter-v1.ts | 24 ++++++++ app/soapbox/normalizers/filter.ts | 33 ++++++++--- app/soapbox/normalizers/index.ts | 3 + app/soapbox/normalizers/status.ts | 17 +++--- app/soapbox/reducers/filters.ts | 18 +++--- app/soapbox/reducers/statuses.ts | 3 + app/soapbox/selectors/index.ts | 42 +++++++++++--- app/soapbox/types/entities.ts | 9 +++ app/soapbox/utils/features.ts | 8 ++- 18 files changed, 259 insertions(+), 60 deletions(-) create mode 100644 app/soapbox/normalizers/filter-keyword.ts create mode 100644 app/soapbox/normalizers/filter-result.ts create mode 100644 app/soapbox/normalizers/filter-status.ts create mode 100644 app/soapbox/normalizers/filter-v1.ts diff --git a/README.md b/README.md index 2504de278..2c419698a 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,10 @@ One disadvantage of this approach is that it does not help the software spread. # License & Credits -© Alex Gleason & other Soapbox contributors -© Eugen Rochko & other Mastodon contributors -© Trump Media & Technology Group -© Gab AI, Inc. +© Alex Gleason & other Soapbox contributors +© Eugen Rochko & other Mastodon contributors +© Trump Media & Technology Group +© Gab AI, Inc. Soapbox is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by diff --git a/app/assets/icons/COPYING.md b/app/assets/icons/COPYING.md index 1dcc928d9..a5dbe7d98 100644 --- a/app/assets/icons/COPYING.md +++ b/app/assets/icons/COPYING.md @@ -2,4 +2,4 @@ - verified.svg - Created by Alex Gleason. CC0 -Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg +Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg diff --git a/app/soapbox/actions/filters.ts b/app/soapbox/actions/filters.ts index 7e663f88d..6d6f82736 100644 --- a/app/soapbox/actions/filters.ts +++ b/app/soapbox/actions/filters.ts @@ -8,9 +8,13 @@ import api from '../api'; import type { AppDispatch, RootState } from 'soapbox/store'; -const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST'; -const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS'; -const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL'; +const FILTERS_V1_FETCH_REQUEST = 'FILTERS_V1_FETCH_REQUEST'; +const FILTERS_V1_FETCH_SUCCESS = 'FILTERS_V1_FETCH_SUCCESS'; +const FILTERS_V1_FETCH_FAIL = 'FILTERS_V1_FETCH_FAIL'; + +const FILTERS_V2_FETCH_REQUEST = 'FILTERS_V2_FETCH_REQUEST'; +const FILTERS_V2_FETCH_SUCCESS = 'FILTERS_V2_FETCH_SUCCESS'; +const FILTERS_V2_FETCH_FAIL = 'FILTERS_V2_FETCH_FAIL'; const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST'; const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS'; @@ -25,36 +29,63 @@ const messages = defineMessages({ removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' }, }); -const fetchFilters = () => +const fetchFiltersV1 = () => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - const state = getState(); - const instance = state.instance; - const features = getFeatures(instance); + dispatch({ + type: FILTERS_V1_FETCH_REQUEST, + skipLoading: true, + }); - if (!features.filters) return; + api(getState) + .get('/api/v1/filters') + .then(({ data }) => dispatch({ + type: FILTERS_V1_FETCH_SUCCESS, + filters: data, + skipLoading: true, + })) + .catch(err => dispatch({ + type: FILTERS_V1_FETCH_FAIL, + err, + skipLoading: true, + skipAlert: true, + })); + }; +const fetchFiltersV2 = () => + (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ - type: FILTERS_FETCH_REQUEST, + type: FILTERS_V2_FETCH_REQUEST, skipLoading: true, }); api(getState) - .get('/api/v1/filters') + .get('/api/v2/filters') .then(({ data }) => dispatch({ - type: FILTERS_FETCH_SUCCESS, + type: FILTERS_V2_FETCH_SUCCESS, filters: data, skipLoading: true, })) .catch(err => dispatch({ - type: FILTERS_FETCH_FAIL, + type: FILTERS_V2_FETCH_FAIL, err, skipLoading: true, skipAlert: true, })); }; +const fetchFilters = () => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + const state = getState(); + const instance = state.instance; + const features = getFeatures(instance); + + if (features.filtersV2) return dispatch(fetchFiltersV2()); + + if (features.filters) return dispatch(fetchFiltersV1()); + }; + const createFilter = (phrase: string, expires_at: string, context: Array, whole_word: boolean, irreversible: boolean) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: FILTERS_CREATE_REQUEST }); @@ -84,9 +115,12 @@ const deleteFilter = (id: string) => }; export { - FILTERS_FETCH_REQUEST, - FILTERS_FETCH_SUCCESS, - FILTERS_FETCH_FAIL, + FILTERS_V1_FETCH_REQUEST, + FILTERS_V1_FETCH_SUCCESS, + FILTERS_V1_FETCH_FAIL, + FILTERS_V2_FETCH_REQUEST, + FILTERS_V2_FETCH_SUCCESS, + FILTERS_V2_FETCH_FAIL, FILTERS_CREATE_REQUEST, FILTERS_CREATE_SUCCESS, FILTERS_CREATE_FAIL, @@ -96,4 +130,4 @@ export { fetchFilters, createFilter, deleteFilter, -}; \ No newline at end of file +}; diff --git a/app/soapbox/actions/statuses.ts b/app/soapbox/actions/statuses.ts index 047d61d71..b14108de2 100644 --- a/app/soapbox/actions/statuses.ts +++ b/app/soapbox/actions/statuses.ts @@ -48,6 +48,8 @@ const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS'; const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL'; const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO'; +const STATUS_UNFILTER = 'STATUS_UNFILTER'; + const statusExists = (getState: () => RootState, statusId: string) => { return (getState().statuses.get(statusId) || null) !== null; }; @@ -335,6 +337,11 @@ const undoStatusTranslation = (id: string) => ({ id, }); +const unfilterStatus = (id: string) => ({ + type: STATUS_UNFILTER, + id, +}); + export { STATUS_CREATE_REQUEST, STATUS_CREATE_SUCCESS, @@ -363,6 +370,7 @@ export { STATUS_TRANSLATE_SUCCESS, STATUS_TRANSLATE_FAIL, STATUS_TRANSLATE_UNDO, + STATUS_UNFILTER, createStatus, editStatus, fetchStatus, @@ -381,4 +389,5 @@ export { toggleStatusHidden, translateStatus, undoStatusTranslation, + unfilterStatus, }; diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 16358f9dd..3ec072394 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -7,7 +7,7 @@ import { useHistory } from 'react-router-dom'; import { mentionCompose, replyCompose } from 'soapbox/actions/compose'; import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions'; import { openModal } from 'soapbox/actions/modals'; -import { toggleStatusHidden } from 'soapbox/actions/statuses'; +import { toggleStatusHidden, unfilterStatus } from 'soapbox/actions/statuses'; import Icon from 'soapbox/components/icon'; import TranslateButton from 'soapbox/components/translate-button'; import AccountContainer from 'soapbox/containers/account-container'; @@ -93,6 +93,8 @@ const Status: React.FC = (props) => { const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`; const group = actualStatus.group as GroupEntity | null; + const filtered = (status.filtered.size || actualStatus.filtered.size) > 0; + // Track height changes we know about to compensate scrolling. useEffect(() => { didShowCard.current = Boolean(!muted && !hidden && status?.card); @@ -202,6 +204,8 @@ const Status: React.FC = (props) => { _expandEmojiSelector(); }; + const handleUnfilter = () => dispatch(unfilterStatus(status.filtered.size ? status.id : actualStatus.id)); + const _expandEmojiSelector = (): void => { const firstEmoji: HTMLDivElement | null | undefined = node.current?.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); firstEmoji?.focus(); @@ -281,7 +285,7 @@ const Status: React.FC = (props) => { ); } - if (status.filtered || actualStatus.filtered) { + if (filtered && status.showFiltered) { const minHandlers = muted ? undefined : { moveUp: handleHotkeyMoveUp, moveDown: handleHotkeyMoveDown, @@ -291,7 +295,11 @@ const Status: React.FC = (props) => {
- + : {status.filtered.join(', ')}. + {' '} +
diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index 2b32f88dc..a3fc6cc0b 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -1131,7 +1131,7 @@ "status.embed": "Osadź", "status.external": "View post on {domain}", "status.favourite": "Zareaguj", - "status.filtered": "Filtrowany(-a)", + "status.filtered": "Filtrowany", "status.interactions.favourites": "{count, plural, one {Polubienie} few {Polubienia} other {Polubień}}", "status.interactions.reblogs": "{count, plural, one {Podanie dalej} few {Podania dalej} other {Podań dalej}}", "status.load_more": "Załaduj więcej", diff --git a/app/soapbox/normalizers/filter-keyword.ts b/app/soapbox/normalizers/filter-keyword.ts new file mode 100644 index 000000000..b81fd4b63 --- /dev/null +++ b/app/soapbox/normalizers/filter-keyword.ts @@ -0,0 +1,18 @@ +/** + * Filter normalizer: + * Converts API filters into our internal format. + * @see {@link https://docs.joinmastodon.org/entities/FilterKeyword/} + */ +import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable'; + +// https://docs.joinmastodon.org/entities/FilterKeyword/ +export const FilterKeywordRecord = ImmutableRecord({ + id: '', + keyword: '', + whole_word: false, +}); + +export const normalizeFilterKeyword = (filterKeyword: Record) => + FilterKeywordRecord( + ImmutableMap(fromJS(filterKeyword)), + ); diff --git a/app/soapbox/normalizers/filter-result.ts b/app/soapbox/normalizers/filter-result.ts new file mode 100644 index 000000000..08ed3499f --- /dev/null +++ b/app/soapbox/normalizers/filter-result.ts @@ -0,0 +1,22 @@ +/** + * Filter normalizer: + * Converts API filters into our internal format. + * @see {@link https://docs.joinmastodon.org/entities/FilterResult/} + */ +import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable'; + +import { normalizeFilter } from './filter'; + +import type { Filter } from 'soapbox/types/entities'; + +// https://docs.joinmastodon.org/entities/FilterResult/ +export const FilterResultRecord = ImmutableRecord({ + filter: null as Filter | null, + keyword_matches: ImmutableList(), + status_matches: ImmutableList(), +}); + +export const normalizeFilterResult = (filterResult: Record) => + FilterResultRecord( + ImmutableMap(fromJS(filterResult)).update('filter', (filter: any) => normalizeFilter(filter) as any), + ); diff --git a/app/soapbox/normalizers/filter-status.ts b/app/soapbox/normalizers/filter-status.ts new file mode 100644 index 000000000..d827d76af --- /dev/null +++ b/app/soapbox/normalizers/filter-status.ts @@ -0,0 +1,17 @@ +/** + * Filter normalizer: + * Converts API filters into our internal format. + * @see {@link https://docs.joinmastodon.org/entities/FilterStatus/} + */ +import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable'; + +// https://docs.joinmastodon.org/entities/FilterStatus/ +export const FilterStatusRecord = ImmutableRecord({ + id: '', + status_id: '', +}); + +export const normalizeFilterStatus = (filterStatus: Record) => + FilterStatusRecord( + ImmutableMap(fromJS(filterStatus)), + ); diff --git a/app/soapbox/normalizers/filter-v1.ts b/app/soapbox/normalizers/filter-v1.ts new file mode 100644 index 000000000..ef454db1e --- /dev/null +++ b/app/soapbox/normalizers/filter-v1.ts @@ -0,0 +1,24 @@ +/** + * Filter normalizer: + * Converts API filters into our internal format. + * @see {@link https://docs.joinmastodon.org/entities/V1_Filter/} + */ +import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable'; + +import type { ContextType } from './filter'; + +// https://docs.joinmastodon.org/entities/V1_Filter/ +export const FilterV1Record = ImmutableRecord({ + id: '', + phrase: '', + context: ImmutableList(), + whole_word: false, + expires_at: '', + irreversible: false, +}); + +export const normalizeFilterV1 = (filter: Record) => { + return FilterV1Record( + ImmutableMap(fromJS(filter)), + ); +}; diff --git a/app/soapbox/normalizers/filter.ts b/app/soapbox/normalizers/filter.ts index 5537acac9..24329945b 100644 --- a/app/soapbox/normalizers/filter.ts +++ b/app/soapbox/normalizers/filter.ts @@ -5,20 +5,39 @@ */ import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable'; +import { FilterKeyword, FilterStatus } from 'soapbox/types/entities'; + +import { normalizeFilterKeyword } from './filter-keyword'; +import { normalizeFilterStatus } from './filter-status'; + export type ContextType = 'home' | 'public' | 'notifications' | 'thread'; +export type FilterActionType = 'warn' | 'hide'; // https://docs.joinmastodon.org/entities/filter/ export const FilterRecord = ImmutableRecord({ id: '', - phrase: '', + title: '', context: ImmutableList(), - whole_word: false, expires_at: '', - irreversible: false, + filter_action: 'warn' as FilterActionType, + keywords: ImmutableList(), + statuses: ImmutableList(), }); -export const normalizeFilter = (filter: Record) => { - return FilterRecord( - ImmutableMap(fromJS(filter)), +const normalizeKeywords = (filter: ImmutableMap) => + filter.update('keywords', ImmutableList(), keywords => + keywords.map(normalizeFilterKeyword), + ); + +const normalizeStatuses = (filter: ImmutableMap) => + filter.update('statuses', ImmutableList(), statuses => + statuses.map(normalizeFilterStatus), + ); + +export const normalizeFilter = (filter: Record) => + FilterRecord( + ImmutableMap(fromJS(filter)).withMutations(filter => { + normalizeKeywords(filter); + normalizeStatuses(filter); + }), ); -}; diff --git a/app/soapbox/normalizers/index.ts b/app/soapbox/normalizers/index.ts index 5b05a0e21..b0dd89088 100644 --- a/app/soapbox/normalizers/index.ts +++ b/app/soapbox/normalizers/index.ts @@ -10,6 +10,9 @@ export { ChatMessageRecord, normalizeChatMessage } from './chat-message'; export { EmojiRecord, normalizeEmoji } from './emoji'; export { EmojiReactionRecord } from './emoji-reaction'; export { FilterRecord, normalizeFilter } from './filter'; +export { FilterKeywordRecord, normalizeFilterKeyword } from './filter-keyword'; +export { FilterStatusRecord, normalizeFilterStatus } from './filter-status'; +export { FilterV1Record, normalizeFilterV1 } from './filter-v1'; export { GroupRecord, normalizeGroup } from './group'; export { GroupRelationshipRecord, normalizeGroupRelationship } from './group-relationship'; export { HistoryRecord, normalizeHistory } from './history'; diff --git a/app/soapbox/normalizers/status.ts b/app/soapbox/normalizers/status.ts index 57e27806f..3de7f6f4f 100644 --- a/app/soapbox/normalizers/status.ts +++ b/app/soapbox/normalizers/status.ts @@ -50,6 +50,7 @@ export const StatusRecord = ImmutableRecord({ emojis: ImmutableList(), favourited: false, favourites_count: 0, + filtered: ImmutableList(), group: null as EmbeddedEntity, in_reply_to_account_id: null as string | null, in_reply_to_id: null as string | null, @@ -78,9 +79,9 @@ export const StatusRecord = ImmutableRecord({ // Internal fields contentHtml: '', expectsCard: false, - filtered: false, hidden: false, search_index: '', + showFiltered: true, spoilerHtml: '', translation: null as ImmutableMap | null, }); @@ -166,11 +167,6 @@ const fixQuote = (status: ImmutableMap) => { }); }; -// Workaround for not yet implemented filtering from Mastodon 3.6 -const fixFiltered = (status: ImmutableMap) => { - status.delete('filtered'); -}; - /** If the status contains spoiler text, treat it as sensitive. */ const fixSensitivity = (status: ImmutableMap) => { if (status.get('spoiler_text')) { @@ -214,6 +210,13 @@ const fixContent = (status: ImmutableMap) => { } }; +const normalizeFilterResults = (status: ImmutableMap) => + status.update('filtered', ImmutableList(), filterResults => + filterResults.map((filterResult: ImmutableMap) => + filterResult.getIn(['filter', 'title']), + ), + ); + export const normalizeStatus = (status: Record) => { return StatusRecord( ImmutableMap(fromJS(status)).withMutations(status => { @@ -225,10 +228,10 @@ export const normalizeStatus = (status: Record) => { fixMentionsOrder(status); addSelfMention(status); fixQuote(status); - fixFiltered(status); fixSensitivity(status); normalizeEvent(status); fixContent(status); + normalizeFilterResults(status); }), ); }; diff --git a/app/soapbox/reducers/filters.ts b/app/soapbox/reducers/filters.ts index a31cb3295..9aaff191c 100644 --- a/app/soapbox/reducers/filters.ts +++ b/app/soapbox/reducers/filters.ts @@ -1,22 +1,22 @@ import { List as ImmutableList } from 'immutable'; -import { normalizeFilter } from 'soapbox/normalizers'; +import { normalizeFilterV1 } from 'soapbox/normalizers'; -import { FILTERS_FETCH_SUCCESS } from '../actions/filters'; +import { FILTERS_V1_FETCH_SUCCESS } from '../actions/filters'; import type { AnyAction } from 'redux'; -import type { APIEntity, Filter as FilterEntity } from 'soapbox/types/entities'; +import type { APIEntity, FilterV1 as FilterV1Entity } from 'soapbox/types/entities'; -type State = ImmutableList; +type State = ImmutableList; -const importFilters = (_state: State, filters: APIEntity[]): State => { - return ImmutableList(filters.map((filter) => normalizeFilter(filter))); +const importFiltersV1 = (_state: State, filters: APIEntity[]): State => { + return ImmutableList(filters.map((filter) => normalizeFilterV1(filter))); }; -export default function filters(state: State = ImmutableList(), action: AnyAction): State { +export default function filters(state: State = ImmutableList(), action: AnyAction): State { switch (action.type) { - case FILTERS_FETCH_SUCCESS: - return importFilters(state, action.filters); + case FILTERS_V1_FETCH_SUCCESS: + return importFiltersV1(state, action.filters); default: return state; } diff --git a/app/soapbox/reducers/statuses.ts b/app/soapbox/reducers/statuses.ts index ed7ee1c2b..17662da86 100644 --- a/app/soapbox/reducers/statuses.ts +++ b/app/soapbox/reducers/statuses.ts @@ -38,6 +38,7 @@ import { STATUS_DELETE_FAIL, STATUS_TRANSLATE_SUCCESS, STATUS_TRANSLATE_UNDO, + STATUS_UNFILTER, } from '../actions/statuses'; import { TIMELINE_DELETE } from '../actions/timelines'; @@ -287,6 +288,8 @@ export default function statuses(state = initialState, action: AnyAction): State return importTranslation(state, action.id, action.translation); case STATUS_TRANSLATE_UNDO: return deleteTranslation(state, action.id); + case STATUS_UNFILTER: + return state.setIn([action.id, 'showFiltered'], false); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.references); case EVENT_JOIN_REQUEST: diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts index 5e518f082..ec5da2d82 100644 --- a/app/soapbox/selectors/index.ts +++ b/app/soapbox/selectors/index.ts @@ -10,12 +10,13 @@ import { getSettings } from 'soapbox/actions/settings'; import { getDomain } from 'soapbox/utils/accounts'; import { validId } from 'soapbox/utils/auth'; import ConfigDB from 'soapbox/utils/config-db'; +import { getFeatures } from 'soapbox/utils/features'; import { shouldFilter } from 'soapbox/utils/timelines'; import type { ContextType } from 'soapbox/normalizers/filter'; import type { ReducerChat } from 'soapbox/reducers/chats'; import type { RootState } from 'soapbox/store'; -import type { Filter as FilterEntity, Notification } from 'soapbox/types/entities'; +import type { FilterV1 as FilterV1Entity, Notification } from 'soapbox/types/entities'; const normalizeId = (id: any): string => typeof id === 'string' ? id : ''; @@ -114,13 +115,13 @@ export const getFilters = (state: RootState, query: FilterContext) => { const escapeRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -export const regexFromFilters = (filters: ImmutableList) => { +export const regexFromFilters = (filters: ImmutableList) => { if (filters.size === 0) return null; return new RegExp(filters.map(filter => { - let expr = escapeRegExp(filter.get('phrase')); + let expr = escapeRegExp(filter.phrase); - if (filter.get('whole_word')) { + if (filter.whole_word) { if (/^[\w]/.test(expr)) { expr = `\\b${expr}`; } @@ -134,6 +135,26 @@ export const regexFromFilters = (filters: ImmutableList) => { }).join('|'), 'i'); }; +const checkFiltered = (index: string, filters: ImmutableList) => + filters.reduce((result, filter) => { + let expr = escapeRegExp(filter.phrase); + + if (filter.whole_word) { + if (/^[\w]/.test(expr)) { + expr = `\\b${expr}`; + } + + if (/[\w]$/.test(expr)) { + expr = `${expr}\\b`; + } + } + + const regex = new RegExp(expr); + + if (regex.test(index)) return result.push(filter.phrase); + return result; + }, ImmutableList()); + type APIStatus = { id: string, username?: string }; export const makeGetStatus = () => { @@ -147,9 +168,10 @@ export const makeGetStatus = () => { (_state: RootState, { username }: APIStatus) => username, getFilters, (state: RootState) => state.me, + (state: RootState) => getFeatures(state.instance), ], - (statusBase, statusReblog, accountBase, accountReblog, group, username, filters, me) => { + (statusBase, statusReblog, accountBase, accountReblog, group, username, filters, me, features) => { if (!statusBase || !accountBase) return null; const accountUsername = accountBase.acct; @@ -165,16 +187,18 @@ export const makeGetStatus = () => { statusReblog = undefined; } - const regex = (accountReblog || accountBase).id !== me && regexFromFilters(filters); - const filtered = regex && regex.test(statusReblog?.search_index || statusBase.search_index); - return statusBase.withMutations(map => { map.set('reblog', statusReblog || null); // @ts-ignore :( map.set('account', accountBase || null); // @ts-ignore map.set('group', group || null); - map.set('filtered', Boolean(filtered)); + + if (features.filters && (accountReblog || accountBase).id !== me) { + const filtered = checkFiltered(statusReblog?.search_index || statusBase.search_index, filters); + + map.set('filtered', filtered); + } }); }, ); diff --git a/app/soapbox/types/entities.ts b/app/soapbox/types/entities.ts index ed43df407..30c94684e 100644 --- a/app/soapbox/types/entities.ts +++ b/app/soapbox/types/entities.ts @@ -12,6 +12,9 @@ import { EmojiReactionRecord, FieldRecord, FilterRecord, + FilterKeywordRecord, + FilterStatusRecord, + FilterV1Record, GroupRecord, GroupRelationshipRecord, HistoryRecord, @@ -44,6 +47,9 @@ type Emoji = ReturnType; type EmojiReaction = ReturnType; type Field = ReturnType; type Filter = ReturnType; +type FilterKeyword = ReturnType; +type FilterStatus = ReturnType; +type FilterV1 = ReturnType; type Group = ReturnType; type GroupRelationship = ReturnType; type History = ReturnType; @@ -89,6 +95,9 @@ export { EmojiReaction, Field, Filter, + FilterKeyword, + FilterStatus, + FilterV1, Group, GroupRelationship, History, diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 3b1c14b77..46ac310ea 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -443,13 +443,19 @@ const getInstanceFeatures = (instance: Instance) => { /** * Can edit and manage timeline filters (aka "muted words"). - * @see {@link https://docs.joinmastodon.org/methods/accounts/filters/} + * @see {@link https://docs.joinmastodon.org/methods/filters/#v1} */ filters: any([ v.software === MASTODON && lt(v.compatVersion, '3.6.0'), v.software === PLEROMA, ]), + /** + * Can edit and manage timeline filters (aka "muted words"). + * @see {@link https://docs.joinmastodon.org/methods/accounts/filters/} + */ + filtersV2: v.software === MASTODON && gte(v.compatVersion, '3.6.0'), + /** * Allows setting the focal point of a media attachment. * @see {@link https://docs.joinmastodon.org/methods/statuses/media/}