Keyword/phrase filtering (#7905)
* Add keyword filtering GET|POST /api/v1/filters GET|PUT|DELETE /api/v1/filters/:id - Irreversible filters can drop toots from home or notifications - Other filters can hide toots through the client app - Filters use a phrase valid in particular contexts, expiration * Make sure expired filters don't get applied client-side * Add missing API methods * Remove "regex filter" from column settings * Add tests * Add test for FeedManager * Add CustomFilter test * Add UI for managing filters * Add streaming API event to allow syncing filters * Fix tests
This commit is contained in:
@ -1,15 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import SettingText from '../../../components/setting_text';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
|
||||
settings: { id: 'home.settings', defaultMessage: 'Column settings' },
|
||||
});
|
||||
|
||||
@injectIntl
|
||||
export default class ColumnSettings extends React.PureComponent {
|
||||
|
||||
@ -21,19 +15,13 @@ export default class ColumnSettings extends React.PureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { settings, onChange, intl } = this.props;
|
||||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
|
||||
</div>
|
||||
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
status: getStatus(state, state.getIn(['compose', 'in_reply_to'])),
|
||||
status: getStatus(state, { id: state.getIn(['compose', 'in_reply_to']) }),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
|
@ -7,7 +7,6 @@ import ColumnHeader from '../../components/column_header';
|
||||
import { expandDirectTimeline } from '../../actions/timelines';
|
||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import { connectDirectStream } from '../../actions/streaming';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -86,9 +85,7 @@ export default class DirectTimeline extends React.PureComponent {
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
>
|
||||
<ColumnSettingsContainer />
|
||||
</ColumnHeader>
|
||||
/>
|
||||
|
||||
<StatusListContainer
|
||||
trackScroll={!pinned}
|
||||
|
@ -1,14 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
import SettingText from '../../../components/setting_text';
|
||||
|
||||
const messages = defineMessages({
|
||||
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
|
||||
settings: { id: 'home.settings', defaultMessage: 'Column settings' },
|
||||
});
|
||||
|
||||
@injectIntl
|
||||
export default class ColumnSettings extends React.PureComponent {
|
||||
@ -20,7 +14,7 @@ export default class ColumnSettings extends React.PureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { settings, onChange, intl } = this.props;
|
||||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -33,12 +27,6 @@ export default class ColumnSettings extends React.PureComponent {
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||
</div>
|
||||
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingText prefix='home_timeline' settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const status = getStatus(state, props.params.statusId);
|
||||
const status = getStatus(state, { id: props.params.statusId });
|
||||
let ancestorsIds = Immutable.List();
|
||||
let descendantsIds = Immutable.List();
|
||||
|
||||
@ -336,6 +336,7 @@ export default class Status extends ImmutablePureComponent {
|
||||
id={id}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType='thread'
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
@ -11,15 +11,6 @@ const makeGetStatusIds = () => createSelector([
|
||||
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
|
||||
(state) => state.get('statuses'),
|
||||
], (columnSettings, statusIds, statuses) => {
|
||||
const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim();
|
||||
let regex = null;
|
||||
|
||||
try {
|
||||
regex = rawRegex && new RegExp(rawRegex, 'i');
|
||||
} catch (e) {
|
||||
// Bad regex, don't affect filters
|
||||
}
|
||||
|
||||
return statusIds.filter(id => {
|
||||
if (id === null) return true;
|
||||
|
||||
@ -34,11 +25,6 @@ const makeGetStatusIds = () => createSelector([
|
||||
showStatus = showStatus && (statusForId.get('in_reply_to_id') === null || statusForId.get('in_reply_to_account_id') === me);
|
||||
}
|
||||
|
||||
if (showStatus && regex && statusForId.get('account') !== me) {
|
||||
const searchIndex = statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'search_index']) : statusForId.get('search_index');
|
||||
showStatus = !regex.test(searchIndex);
|
||||
}
|
||||
|
||||
return showStatus;
|
||||
});
|
||||
});
|
||||
|
@ -12,6 +12,7 @@ import { debounce } from 'lodash';
|
||||
import { uploadCompose, resetCompose } from '../../actions/compose';
|
||||
import { expandHomeTimeline } from '../../actions/timelines';
|
||||
import { expandNotifications } from '../../actions/notifications';
|
||||
import { fetchFilters } from '../../actions/filters';
|
||||
import { clearHeight } from '../../actions/height_cache';
|
||||
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||
import UploadArea from './components/upload_area';
|
||||
@ -297,6 +298,7 @@ export default class UI extends React.PureComponent {
|
||||
|
||||
this.props.dispatch(expandHomeTimeline());
|
||||
this.props.dispatch(expandNotifications());
|
||||
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
|
Reference in New Issue
Block a user