filters v2
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
aad7df89a5
commit
4200fa2df4
18 changed files with 267 additions and 68 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,6 +29,50 @@ const messages = defineMessages({
|
|||
removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' },
|
||||
});
|
||||
|
||||
const fetchFiltersV1 = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: FILTERS_V1_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
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_V2_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
api(getState)
|
||||
.get('/api/v2/filters')
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTERS_V2_FETCH_SUCCESS,
|
||||
filters: data,
|
||||
skipLoading: true,
|
||||
}))
|
||||
.catch(err => dispatch({
|
||||
type: FILTERS_V2_FETCH_FAIL,
|
||||
err,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchFilters = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
@ -33,26 +81,9 @@ const fetchFilters = () =>
|
|||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (!features.filters) return;
|
||||
if (features.filtersV2) return dispatch(fetchFiltersV2());
|
||||
|
||||
dispatch({
|
||||
type: FILTERS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
api(getState)
|
||||
.get('/api/v1/filters')
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTERS_FETCH_SUCCESS,
|
||||
filters: data,
|
||||
skipLoading: true,
|
||||
}))
|
||||
.catch(err => dispatch({
|
||||
type: FILTERS_FETCH_FAIL,
|
||||
err,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
}));
|
||||
if (features.filters) return dispatch(fetchFiltersV1());
|
||||
};
|
||||
|
||||
const createFilter = (phrase: string, expires_at: string, context: Array<string>, whole_word: boolean, irreversible: boolean) =>
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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<IStatus> = (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<IStatus> = (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<IStatus> = (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<IStatus> = (props) => {
|
|||
<HotKeys handlers={minHandlers}>
|
||||
<div className={clsx('status__wrapper text-center', { focusable })} tabIndex={focusable ? 0 : undefined} ref={node}>
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {status.filtered.join(', ')}.
|
||||
{' '}
|
||||
<button className='text-primary-600 hover:underline dark:text-accent-blue' onClick={handleUnfilter}>
|
||||
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
|
||||
</button>
|
||||
</Text>
|
||||
</div>
|
||||
</HotKeys>
|
||||
|
|
|
@ -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",
|
||||
|
|
18
app/soapbox/normalizers/filter-keyword.ts
Normal file
18
app/soapbox/normalizers/filter-keyword.ts
Normal file
|
@ -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<string, any>) =>
|
||||
FilterKeywordRecord(
|
||||
ImmutableMap(fromJS(filterKeyword)),
|
||||
);
|
22
app/soapbox/normalizers/filter-result.ts
Normal file
22
app/soapbox/normalizers/filter-result.ts
Normal file
|
@ -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<string>(),
|
||||
status_matches: ImmutableList<string>(),
|
||||
});
|
||||
|
||||
export const normalizeFilterResult = (filterResult: Record<string, any>) =>
|
||||
FilterResultRecord(
|
||||
ImmutableMap(fromJS(filterResult)).update('filter', (filter: any) => normalizeFilter(filter) as any),
|
||||
);
|
17
app/soapbox/normalizers/filter-status.ts
Normal file
17
app/soapbox/normalizers/filter-status.ts
Normal file
|
@ -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<string, any>) =>
|
||||
FilterStatusRecord(
|
||||
ImmutableMap(fromJS(filterStatus)),
|
||||
);
|
24
app/soapbox/normalizers/filter-v1.ts
Normal file
24
app/soapbox/normalizers/filter-v1.ts
Normal file
|
@ -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<ContextType>(),
|
||||
whole_word: false,
|
||||
expires_at: '',
|
||||
irreversible: false,
|
||||
});
|
||||
|
||||
export const normalizeFilterV1 = (filter: Record<string, any>) => {
|
||||
return FilterV1Record(
|
||||
ImmutableMap(fromJS(filter)),
|
||||
);
|
||||
};
|
|
@ -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<ContextType>(),
|
||||
whole_word: false,
|
||||
expires_at: '',
|
||||
irreversible: false,
|
||||
filter_action: 'warn' as FilterActionType,
|
||||
keywords: ImmutableList<FilterKeyword>(),
|
||||
statuses: ImmutableList<FilterStatus>(),
|
||||
});
|
||||
|
||||
export const normalizeFilter = (filter: Record<string, any>) => {
|
||||
return FilterRecord(
|
||||
ImmutableMap(fromJS(filter)),
|
||||
const normalizeKeywords = (filter: ImmutableMap<string, any>) =>
|
||||
filter.update('keywords', ImmutableList(), keywords =>
|
||||
keywords.map(normalizeFilterKeyword),
|
||||
);
|
||||
|
||||
const normalizeStatuses = (filter: ImmutableMap<string, any>) =>
|
||||
filter.update('statuses', ImmutableList(), statuses =>
|
||||
statuses.map(normalizeFilterStatus),
|
||||
);
|
||||
|
||||
export const normalizeFilter = (filter: Record<string, any>) =>
|
||||
FilterRecord(
|
||||
ImmutableMap(fromJS(filter)).withMutations(filter => {
|
||||
normalizeKeywords(filter);
|
||||
normalizeStatuses(filter);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -50,6 +50,7 @@ export const StatusRecord = ImmutableRecord({
|
|||
emojis: ImmutableList<Emoji>(),
|
||||
favourited: false,
|
||||
favourites_count: 0,
|
||||
filtered: ImmutableList<string>(),
|
||||
group: null as EmbeddedEntity<Group>,
|
||||
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<string, string> | null,
|
||||
});
|
||||
|
@ -166,11 +167,6 @@ const fixQuote = (status: ImmutableMap<string, any>) => {
|
|||
});
|
||||
};
|
||||
|
||||
// Workaround for not yet implemented filtering from Mastodon 3.6
|
||||
const fixFiltered = (status: ImmutableMap<string, any>) => {
|
||||
status.delete('filtered');
|
||||
};
|
||||
|
||||
/** If the status contains spoiler text, treat it as sensitive. */
|
||||
const fixSensitivity = (status: ImmutableMap<string, any>) => {
|
||||
if (status.get('spoiler_text')) {
|
||||
|
@ -214,6 +210,13 @@ const fixContent = (status: ImmutableMap<string, any>) => {
|
|||
}
|
||||
};
|
||||
|
||||
const normalizeFilterResults = (status: ImmutableMap<string, any>) =>
|
||||
status.update('filtered', ImmutableList(), filterResults =>
|
||||
filterResults.map((filterResult: ImmutableMap<string, any>) =>
|
||||
filterResult.getIn(['filter', 'title']),
|
||||
),
|
||||
);
|
||||
|
||||
export const normalizeStatus = (status: Record<string, any>) => {
|
||||
return StatusRecord(
|
||||
ImmutableMap(fromJS(status)).withMutations(status => {
|
||||
|
@ -225,10 +228,10 @@ export const normalizeStatus = (status: Record<string, any>) => {
|
|||
fixMentionsOrder(status);
|
||||
addSelfMention(status);
|
||||
fixQuote(status);
|
||||
fixFiltered(status);
|
||||
fixSensitivity(status);
|
||||
normalizeEvent(status);
|
||||
fixContent(status);
|
||||
normalizeFilterResults(status);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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<FilterEntity>;
|
||||
type State = ImmutableList<FilterV1Entity>;
|
||||
|
||||
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<FilterEntity>(), 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;
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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<FilterEntity>) => {
|
||||
export const regexFromFilters = (filters: ImmutableList<FilterV1Entity>) => {
|
||||
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<FilterEntity>) => {
|
|||
}).join('|'), 'i');
|
||||
};
|
||||
|
||||
const checkFiltered = (index: string, filters: ImmutableList<FilterV1Entity>) =>
|
||||
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<string>());
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
@ -12,6 +12,9 @@ import {
|
|||
EmojiReactionRecord,
|
||||
FieldRecord,
|
||||
FilterRecord,
|
||||
FilterKeywordRecord,
|
||||
FilterStatusRecord,
|
||||
FilterV1Record,
|
||||
GroupRecord,
|
||||
GroupRelationshipRecord,
|
||||
HistoryRecord,
|
||||
|
@ -44,6 +47,9 @@ type Emoji = ReturnType<typeof EmojiRecord>;
|
|||
type EmojiReaction = ReturnType<typeof EmojiReactionRecord>;
|
||||
type Field = ReturnType<typeof FieldRecord>;
|
||||
type Filter = ReturnType<typeof FilterRecord>;
|
||||
type FilterKeyword = ReturnType<typeof FilterKeywordRecord>;
|
||||
type FilterStatus = ReturnType<typeof FilterStatusRecord>;
|
||||
type FilterV1 = ReturnType<typeof FilterV1Record>;
|
||||
type Group = ReturnType<typeof GroupRecord>;
|
||||
type GroupRelationship = ReturnType<typeof GroupRelationshipRecord>;
|
||||
type History = ReturnType<typeof HistoryRecord>;
|
||||
|
@ -89,6 +95,9 @@ export {
|
|||
EmojiReaction,
|
||||
Field,
|
||||
Filter,
|
||||
FilterKeyword,
|
||||
FilterStatus,
|
||||
FilterV1,
|
||||
Group,
|
||||
GroupRelationship,
|
||||
History,
|
||||
|
|
|
@ -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/}
|
||||
|
|
Loading…
Reference in a new issue