forked from treehouse/mastodon
[Glitch] Revamp post filtering system
Port front-end changes from 02851848e9
to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
rebase/4.0.0rc1
parent
4be626734d
commit
a15fe32e61
|
@ -1,26 +0,0 @@
|
||||||
import api from 'flavours/glitch/util/api';
|
|
||||||
|
|
||||||
export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
|
|
||||||
export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
|
|
||||||
export const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
|
|
||||||
|
|
||||||
export const fetchFilters = () => (dispatch, getState) => {
|
|
||||||
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,
|
|
||||||
}));
|
|
||||||
};
|
|
|
@ -5,6 +5,7 @@ export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
|
||||||
export const STATUS_IMPORT = 'STATUS_IMPORT';
|
export const STATUS_IMPORT = 'STATUS_IMPORT';
|
||||||
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
|
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
|
||||||
export const POLLS_IMPORT = 'POLLS_IMPORT';
|
export const POLLS_IMPORT = 'POLLS_IMPORT';
|
||||||
|
export const FILTERS_IMPORT = 'FILTERS_IMPORT';
|
||||||
|
|
||||||
function pushUnique(array, object) {
|
function pushUnique(array, object) {
|
||||||
if (array.every(element => element.id !== object.id)) {
|
if (array.every(element => element.id !== object.id)) {
|
||||||
|
@ -28,6 +29,10 @@ export function importStatuses(statuses) {
|
||||||
return { type: STATUSES_IMPORT, statuses };
|
return { type: STATUSES_IMPORT, statuses };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function importFilters(filters) {
|
||||||
|
return { type: FILTERS_IMPORT, filters };
|
||||||
|
}
|
||||||
|
|
||||||
export function importPolls(polls) {
|
export function importPolls(polls) {
|
||||||
return { type: POLLS_IMPORT, polls };
|
return { type: POLLS_IMPORT, polls };
|
||||||
}
|
}
|
||||||
|
@ -61,11 +66,16 @@ export function importFetchedStatuses(statuses) {
|
||||||
const accounts = [];
|
const accounts = [];
|
||||||
const normalStatuses = [];
|
const normalStatuses = [];
|
||||||
const polls = [];
|
const polls = [];
|
||||||
|
const filters = [];
|
||||||
|
|
||||||
function processStatus(status) {
|
function processStatus(status) {
|
||||||
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), getState().get('local_settings')));
|
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), getState().get('local_settings')));
|
||||||
pushUnique(accounts, status.account);
|
pushUnique(accounts, status.account);
|
||||||
|
|
||||||
|
if (status.filtered) {
|
||||||
|
status.filtered.forEach(result => pushUnique(filters, result.filter));
|
||||||
|
}
|
||||||
|
|
||||||
if (status.reblog && status.reblog.id) {
|
if (status.reblog && status.reblog.id) {
|
||||||
processStatus(status.reblog);
|
processStatus(status.reblog);
|
||||||
}
|
}
|
||||||
|
@ -80,6 +90,7 @@ export function importFetchedStatuses(statuses) {
|
||||||
dispatch(importPolls(polls));
|
dispatch(importPolls(polls));
|
||||||
dispatch(importFetchedAccounts(accounts));
|
dispatch(importFetchedAccounts(accounts));
|
||||||
dispatch(importStatuses(normalStatuses));
|
dispatch(importStatuses(normalStatuses));
|
||||||
|
dispatch(importFilters(filters));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,14 @@ export function normalizeAccount(account) {
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeFilterResult(result) {
|
||||||
|
const normalResult = { ...result };
|
||||||
|
|
||||||
|
normalResult.filter = normalResult.filter.id;
|
||||||
|
|
||||||
|
return normalResult;
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeStatus(status, normalOldStatus, settings) {
|
export function normalizeStatus(status, normalOldStatus, settings) {
|
||||||
const normalStatus = { ...status };
|
const normalStatus = { ...status };
|
||||||
normalStatus.account = status.account.id;
|
normalStatus.account = status.account.id;
|
||||||
|
@ -54,6 +62,10 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
||||||
normalStatus.poll = status.poll.id;
|
normalStatus.poll = status.poll.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.filtered) {
|
||||||
|
normalStatus.filtered = status.filtered.map(normalizeFilterResult);
|
||||||
|
}
|
||||||
|
|
||||||
// Only calculate these values when status first encountered and
|
// Only calculate these values when status first encountered and
|
||||||
// when the underlying values change. Otherwise keep the ones
|
// when the underlying values change. Otherwise keep the ones
|
||||||
// already in the reducer
|
// already in the reducer
|
||||||
|
|
|
@ -12,10 +12,8 @@ import { saveSettings } from './settings';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { unescapeHTML } from 'flavours/glitch/util/html';
|
import { unescapeHTML } from 'flavours/glitch/util/html';
|
||||||
import { getFiltersRegex } from 'flavours/glitch/selectors';
|
|
||||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state';
|
import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state';
|
||||||
import compareId from 'flavours/glitch/util/compare_id';
|
import compareId from 'flavours/glitch/util/compare_id';
|
||||||
import { searchTextFromRawStatus } from 'flavours/glitch/actions/importer/normalizer';
|
|
||||||
import { requestNotificationPermission } from 'flavours/glitch/util/notifications';
|
import { requestNotificationPermission } from 'flavours/glitch/util/notifications';
|
||||||
|
|
||||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||||
|
@ -74,20 +72,17 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type;
|
const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type;
|
||||||
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
||||||
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
||||||
const filters = getFiltersRegex(getState(), { contextType: 'notifications' });
|
|
||||||
|
|
||||||
let filtered = false;
|
let filtered = false;
|
||||||
|
|
||||||
if (['mention', 'status'].includes(notification.type)) {
|
if (['mention', 'status'].includes(notification.type) && notification.status.filtered) {
|
||||||
const dropRegex = filters[0];
|
const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications'));
|
||||||
const regex = filters[1];
|
|
||||||
const searchIndex = searchTextFromRawStatus(notification.status);
|
|
||||||
|
|
||||||
if (dropRegex && dropRegex.test(searchIndex)) {
|
if (filters.some(result => result.filter.filter_action === 'hide')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
filtered = regex && regex.test(searchIndex);
|
filtered = filters.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['follow_request'].includes(notification.type)) {
|
if (['follow_request'].includes(notification.type)) {
|
||||||
|
|
|
@ -21,7 +21,6 @@ import {
|
||||||
updateReaction as updateAnnouncementsReaction,
|
updateReaction as updateAnnouncementsReaction,
|
||||||
deleteAnnouncement,
|
deleteAnnouncement,
|
||||||
} from './announcements';
|
} from './announcements';
|
||||||
import { fetchFilters } from './filters';
|
|
||||||
import { getLocale } from 'mastodon/locales';
|
import { getLocale } from 'mastodon/locales';
|
||||||
|
|
||||||
const { messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
|
@ -97,9 +96,6 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
case 'conversation':
|
case 'conversation':
|
||||||
dispatch(updateConversations(JSON.parse(data.payload)));
|
dispatch(updateConversations(JSON.parse(data.payload)));
|
||||||
break;
|
break;
|
||||||
case 'filters_changed':
|
|
||||||
dispatch(fetchFilters());
|
|
||||||
break;
|
|
||||||
case 'announcement':
|
case 'announcement':
|
||||||
dispatch(updateAnnouncements(JSON.parse(data.payload)));
|
dispatch(updateAnnouncements(JSON.parse(data.payload)));
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -455,8 +455,8 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnfilterClick = e => {
|
handleUnfilterClick = e => {
|
||||||
const { onUnfilter, status } = this.props;
|
this.setState({ forceFilter: false });
|
||||||
onUnfilter(status.get('reblog') ? status.get('reblog') : status, () => this.setState({ forceFilter: false }));
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFilterClick = () => {
|
handleFilterClick = () => {
|
||||||
|
@ -557,8 +557,8 @@ class Status extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = (status.get('filtered') || status.getIn(['reblog', 'filtered'])) && settings.get('filtering_behavior') !== 'content_warning';
|
const matchedFilters = status.get('filtered') || status.getIn(['reblog', 'filtered']);
|
||||||
if (forceFilter === undefined ? filtered : forceFilter) {
|
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
|
||||||
const minHandlers = this.props.muted ? {} : {
|
const minHandlers = this.props.muted ? {} : {
|
||||||
moveUp: this.handleHotkeyMoveUp,
|
moveUp: this.handleHotkeyMoveUp,
|
||||||
moveDown: this.handleHotkeyMoveDown,
|
moveDown: this.handleHotkeyMoveDown,
|
||||||
|
@ -567,13 +567,11 @@ class Status extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={minHandlers}>
|
<HotKeys handlers={minHandlers}>
|
||||||
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
|
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
|
||||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
|
||||||
{settings.get('filtering_behavior') !== 'upstream' && ' '}
|
{' '}
|
||||||
{settings.get('filtering_behavior') !== 'upstream' && (
|
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
|
||||||
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
|
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
|
||||||
<FormattedMessage id='status.show_filter_reason' defaultMessage='(show why)' />
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
|
@ -789,11 +787,11 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
{!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? (
|
{!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? (
|
||||||
<StatusActionBar
|
<StatusActionBar
|
||||||
{...other}
|
|
||||||
status={status}
|
status={status}
|
||||||
account={status.get('account')}
|
account={status.get('account')}
|
||||||
showReplyCount={settings.get('show_reply_count')}
|
showReplyCount={settings.get('show_reply_count')}
|
||||||
onFilter={this.handleFilterClick}
|
onFilter={matchedFilters && this.handleFilterClick}
|
||||||
|
{...other}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{notification ? (
|
{notification ? (
|
||||||
|
|
|
@ -271,10 +271,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
|
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterButton = status.get('filtered') && (
|
|
||||||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleFilterClick} />
|
|
||||||
);
|
|
||||||
|
|
||||||
let replyButton = (
|
let replyButton = (
|
||||||
<IconButton
|
<IconButton
|
||||||
className='status__action-bar-button'
|
className='status__action-bar-button'
|
||||||
|
@ -309,6 +305,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
reblogTitle = intl.formatMessage(messages.cannot_reblog);
|
reblogTitle = intl.formatMessage(messages.cannot_reblog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filterButton = this.props.onFilter && (
|
||||||
|
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleFilterClick} />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
{replyButton}
|
{replyButton}
|
||||||
|
@ -316,6 +316,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||||
{shareButton}
|
{shareButton}
|
||||||
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
||||||
|
|
||||||
{filterButton}
|
{filterButton}
|
||||||
|
|
||||||
<div className='status__action-bar-dropdown'>
|
<div className='status__action-bar-dropdown'>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Status from 'flavours/glitch/components/status';
|
import Status from 'flavours/glitch/components/status';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { makeGetStatus, regexFromFilters, toServerSideType } from 'flavours/glitch/selectors';
|
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||||
import {
|
import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
mentionCompose,
|
mentionCompose,
|
||||||
|
@ -201,48 +201,6 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
dispatch(initBlockModal(account));
|
dispatch(initBlockModal(account));
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnfilter (status, onConfirm) {
|
|
||||||
dispatch((_, getState) => {
|
|
||||||
let state = getState();
|
|
||||||
const serverSideType = toServerSideType(contextType);
|
|
||||||
const enabledFilters = state.get('filters', ImmutableList()).filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))).toArray();
|
|
||||||
const searchIndex = status.get('search_index');
|
|
||||||
const matchingFilters = enabledFilters.filter(filter => regexFromFilters([filter]).test(searchIndex));
|
|
||||||
dispatch(openModal('CONFIRM', {
|
|
||||||
message: [
|
|
||||||
<FormattedMessage id='confirmations.unfilter' defaultMessage='Information about this filtered toot' />,
|
|
||||||
<div className='filtered-status-info'>
|
|
||||||
<Spoilers spoilerText={intl.formatMessage(messages.author)}>
|
|
||||||
<AccountContainer id={status.getIn(['account', 'id'])} />
|
|
||||||
</Spoilers>
|
|
||||||
<Spoilers spoilerText={intl.formatMessage(messages.matchingFilters, {count: matchingFilters.size})}>
|
|
||||||
<ul>
|
|
||||||
{matchingFilters.map(filter => (
|
|
||||||
<li>
|
|
||||||
{filter.get('phrase')}
|
|
||||||
{!!filterEditLink && ' '}
|
|
||||||
{!!filterEditLink && (
|
|
||||||
<a
|
|
||||||
target='_blank'
|
|
||||||
className='filtered-status-edit-link'
|
|
||||||
title={intl.formatMessage(messages.editFilter)}
|
|
||||||
href={filterEditLink(filter.get('id'))}
|
|
||||||
>
|
|
||||||
<Icon id='pencil' />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Spoilers>
|
|
||||||
</div>
|
|
||||||
],
|
|
||||||
confirm: intl.formatMessage(messages.unfilterConfirm),
|
|
||||||
onConfirm: onConfirm,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onReport (status) {
|
onReport (status) {
|
||||||
dispatch(initReport(status.get('account'), status));
|
dispatch(initReport(status.get('account'), status));
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { debounce } from 'lodash';
|
||||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose';
|
import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose';
|
||||||
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
|
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications';
|
import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications';
|
||||||
import { fetchFilters } from 'flavours/glitch/actions/filters';
|
|
||||||
import { fetchRules } from 'flavours/glitch/actions/rules';
|
import { fetchRules } from 'flavours/glitch/actions/rules';
|
||||||
import { clearHeight } from 'flavours/glitch/actions/height_cache';
|
import { clearHeight } from 'flavours/glitch/actions/height_cache';
|
||||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
|
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
|
||||||
|
@ -402,7 +401,7 @@ class UI extends React.Component {
|
||||||
this.props.dispatch(fetchMarkers());
|
this.props.dispatch(fetchMarkers());
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
|
|
||||||
setTimeout(() => this.props.dispatch(fetchRules()), 3000);
|
setTimeout(() => this.props.dispatch(fetchRules()), 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,34 @@
|
||||||
import { FILTERS_FETCH_SUCCESS } from '../actions/filters';
|
import { FILTERS_IMPORT } from '../actions/importer';
|
||||||
import { List as ImmutableList, fromJS } from 'immutable';
|
import { Map as ImmutableMap, is, fromJS } from 'immutable';
|
||||||
|
|
||||||
export default function filters(state = ImmutableList(), action) {
|
const normalizeFilter = (state, filter) => {
|
||||||
|
const normalizedFilter = fromJS({
|
||||||
|
id: filter.id,
|
||||||
|
title: filter.title,
|
||||||
|
context: filter.context,
|
||||||
|
filter_action: filter.filter_action,
|
||||||
|
expires_at: filter.expires_at ? Date.parse(filter.expires_at) : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (is(state.get(filter.id), normalizedFilter)) {
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
return state.set(filter.id, normalizedFilter);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeFilters = (state, filters) => {
|
||||||
|
filters.forEach(filter => {
|
||||||
|
state = normalizeFilter(state, filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function filters(state = ImmutableMap(), action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case FILTERS_FETCH_SUCCESS:
|
case FILTERS_IMPORT:
|
||||||
return fromJS(action.filters);
|
return normalizeFilters(state, action.filters);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,15 +41,15 @@ export const toServerSideType = columnType => {
|
||||||
const escapeRegExp = string =>
|
const escapeRegExp = string =>
|
||||||
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||||
|
|
||||||
export const regexFromFilters = filters => {
|
const regexFromKeywords = keywords => {
|
||||||
if (filters.size === 0) {
|
if (keywords.size === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RegExp(filters.map(filter => {
|
return new RegExp(keywords.map(keyword_filter => {
|
||||||
let expr = escapeRegExp(filter.get('phrase'));
|
let expr = escapeRegExp(keyword_filter.get('keyword'));
|
||||||
|
|
||||||
if (filter.get('whole_word')) {
|
if (keyword_filter.get('whole_word')) {
|
||||||
if (/^[\w]/.test(expr)) {
|
if (/^[\w]/.test(expr)) {
|
||||||
expr = `\\b${expr}`;
|
expr = `\\b${expr}`;
|
||||||
}
|
}
|
||||||
|
@ -63,27 +63,15 @@ export const regexFromFilters = filters => {
|
||||||
}).join('|'), 'i');
|
}).join('|'), 'i');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Memoize the filter regexps for each valid server contextType
|
const getFilters = (state, { contextType }) => {
|
||||||
const makeGetFiltersRegex = () => {
|
if (!contextType) return null;
|
||||||
let memo = {};
|
|
||||||
|
|
||||||
return (state, { contextType }) => {
|
const serverSideType = toServerSideType(contextType);
|
||||||
if (!contextType) return ImmutableList();
|
const now = new Date();
|
||||||
|
|
||||||
const serverSideType = toServerSideType(contextType);
|
return state.get('filters').filter((filter) => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || filter.get('expires_at') > now));
|
||||||
const filters = state.get('filters', ImmutableList()).filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date())));
|
|
||||||
|
|
||||||
if (!memo[serverSideType] || !is(memo[serverSideType].filters, filters)) {
|
|
||||||
const dropRegex = regexFromFilters(filters.filter(filter => filter.get('irreversible')));
|
|
||||||
const regex = regexFromFilters(filters);
|
|
||||||
memo[serverSideType] = { filters: filters, results: [dropRegex, regex] };
|
|
||||||
}
|
|
||||||
return memo[serverSideType].results;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFiltersRegex = makeGetFiltersRegex();
|
|
||||||
|
|
||||||
export const makeGetStatus = () => {
|
export const makeGetStatus = () => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
[
|
[
|
||||||
|
@ -91,56 +79,32 @@ export const makeGetStatus = () => {
|
||||||
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
|
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
||||||
(state, _) => state.getIn(['local_settings', 'filtering_behavior']),
|
getFilters,
|
||||||
(state, _) => state.get('filters', ImmutableList()),
|
|
||||||
(_, { contextType }) => contextType,
|
|
||||||
getFiltersRegex,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
(statusBase, statusReblog, accountBase, accountReblog, filteringBehavior, filters, contextType, filtersRegex) => {
|
(statusBase, statusReblog, accountBase, accountReblog, filters) => {
|
||||||
if (!statusBase) {
|
if (!statusBase) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dropRegex = (accountReblog || accountBase).get('id') !== me && filtersRegex[0];
|
let filtered = false;
|
||||||
|
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
||||||
if (dropRegex && dropRegex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index'))) {
|
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
||||||
return null;
|
if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!filterResults.isEmpty()) {
|
||||||
|
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const regex = (accountReblog || accountBase).get('id') !== me && filtersRegex[1];
|
|
||||||
let filtered = false;
|
|
||||||
|
|
||||||
if (statusReblog) {
|
if (statusReblog) {
|
||||||
filtered = regex && regex.test(statusReblog.get('search_index'));
|
|
||||||
statusReblog = statusReblog.set('account', accountReblog);
|
statusReblog = statusReblog.set('account', accountReblog);
|
||||||
statusReblog = statusReblog.set('filtered', filtered);
|
statusReblog = statusReblog.set('filtered', filtered);
|
||||||
} else {
|
} else {
|
||||||
statusReblog = null;
|
statusReblog = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
filtered = filtered || regex && regex.test(statusBase.get('search_index'));
|
|
||||||
|
|
||||||
if (filtered && filteringBehavior === 'drop') {
|
|
||||||
return null;
|
|
||||||
} else if (filtered && filteringBehavior === 'content_warning') {
|
|
||||||
let spoilerText = (statusReblog || statusBase).get('spoiler_text', '');
|
|
||||||
const searchIndex = (statusReblog || statusBase).get('search_index');
|
|
||||||
const serverSideType = toServerSideType(contextType);
|
|
||||||
const enabledFilters = filters.filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))).toArray();
|
|
||||||
const matchingFilters = enabledFilters.filter(filter => {
|
|
||||||
const regexp = regexFromFilters([filter]);
|
|
||||||
return regexp.test(searchIndex) && !regexp.test(spoilerText);
|
|
||||||
});
|
|
||||||
if (statusReblog) {
|
|
||||||
statusReblog = statusReblog.set('spoiler_text', matchingFilters.map(filter => filter.get('phrase')).concat([spoilerText]).filter(cw => !!cw).join(', '));
|
|
||||||
statusReblog = statusReblog.update('spoilerHtml', '', spoilerText => matchingFilters.map(filter => escapeTextContentForBrowser(filter.get('phrase'))).concat([spoilerText]).filter(cw => !!cw).join(', '));
|
|
||||||
} else {
|
|
||||||
statusBase = statusBase.set('spoiler_text', matchingFilters.map(filter => filter.get('phrase')).concat([spoilerText]).filter(cw => !!cw).join(', '));
|
|
||||||
statusBase = statusBase.update('spoilerHtml', '', spoilerText => matchingFilters.map(filter => escapeTextContentForBrowser(filter.get('phrase'))).concat([spoilerText]).filter(cw => !!cw).join(', '));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return statusBase.withMutations(map => {
|
return statusBase.withMutations(map => {
|
||||||
map.set('reblog', statusReblog);
|
map.set('reblog', statusReblog);
|
||||||
map.set('account', accountBase);
|
map.set('account', accountBase);
|
||||||
|
|
Loading…
Reference in New Issue