Add client-side custom filter support to glitch-soc

Port cdb101340a to glitch-soc,
but without dropping support for regexp filters yet.
This commit is contained in:
Thibaut Girka 2018-07-08 20:04:53 +02:00 committed by ThibG
parent 33c1607c83
commit 0bb1720495
11 changed files with 119 additions and 8 deletions

View File

@ -0,0 +1,26 @@
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,
}));
};

View File

@ -6,6 +6,7 @@ import {
disconnectTimeline,
} from './timelines';
import { updateNotifications, expandNotifications } from './notifications';
import { fetchFilters } from './filters';
import { getLocale } from 'mastodon/locales';
const { messages } = getLocale();
@ -30,6 +31,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
case 'notification':
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break;
case 'filters_changed':
dispatch(fetchFilters());
break;
}
},
};

View File

@ -7,6 +7,7 @@ import StatusIcons from './status_icons';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
import { HotKeys } from 'react-hotkeys';
@ -365,6 +366,21 @@ export default class Status extends ImmutablePureComponent {
);
}
if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) {
const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
};
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0'>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
</div>
</HotKeys>
);
}
// If user backgrounds for collapsed statuses are enabled, then we
// initialize our background accordingly. This will only be rendered if
// the status is collapsed.

View File

@ -24,6 +24,7 @@ export default class StatusList extends ImmutablePureComponent {
hasMore: PropTypes.bool,
prepend: PropTypes.node,
emptyMessage: PropTypes.node,
timelineId: PropTypes.string.isRequired,
};
static defaultProps = {
@ -69,7 +70,7 @@ export default class StatusList extends ImmutablePureComponent {
}
render () {
const { statusIds, featuredStatusIds, onLoadMore, ...other } = this.props;
const { statusIds, featuredStatusIds, onLoadMore, timelineId, ...other } = this.props;
const { isLoading, isPartial } = other;
if (isPartial) {
@ -101,6 +102,7 @@ export default class StatusList extends ImmutablePureComponent {
id={statusId}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType={timelineId}
/>
))
) : null;
@ -113,6 +115,7 @@ export default class StatusList extends ImmutablePureComponent {
featured
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType={timelineId}
/>
)).concat(scrollableContent);
}

View File

@ -38,7 +38,7 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => {
let status = getStatus(state, props.id);
let status = getStatus(state, props);
let reblogStatus = status ? status.get('reblog', null) : null;
let account = undefined;
let prepend = undefined;

View File

@ -53,7 +53,7 @@ const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = (state, props) => ({
status: getStatus(state, props.params.statusId),
status: getStatus(state, { id: props.params.statusId }),
settings: state.get('local_settings'),
ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]),
@ -304,6 +304,7 @@ export default class Status extends ImmutablePureComponent {
expanded={this.state.threadExpanded}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
/>
));
}

View File

@ -11,6 +11,7 @@ import { debounce } from 'lodash';
import { uploadCompose, resetCompose } from 'flavours/glitch/actions/compose';
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
import { expandNotifications } from 'flavours/glitch/actions/notifications';
import { fetchFilters } from 'flavours/glitch/actions/filters';
import { clearHeight } from 'flavours/glitch/actions/height_cache';
import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers';
import UploadArea from './components/upload_area';
@ -218,6 +219,7 @@ export default class UI extends React.Component {
this.props.dispatch(expandHomeTimeline());
this.props.dispatch(expandNotifications());
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
}
componentDidMount () {

View File

@ -0,0 +1,11 @@
import { FILTERS_FETCH_SUCCESS } from '../actions/filters';
import { List as ImmutableList, fromJS } from 'immutable';
export default function filters(state = ImmutableList(), action) {
switch(action.type) {
case FILTERS_FETCH_SUCCESS:
return fromJS(action.filters);
default:
return state;
}
};

View File

@ -27,6 +27,7 @@ import height_cache from './height_cache';
import custom_emojis from './custom_emojis';
import lists from './lists';
import listEditor from './list_editor';
import filters from './filters';
const reducers = {
dropdown_menu,
@ -57,6 +58,7 @@ const reducers = {
custom_emojis,
lists,
listEditor,
filters,
};
export default combineReducers(reducers);

View File

@ -19,16 +19,44 @@ export const makeGetAccount = () => {
});
};
const toServerSideType = columnType => {
switch (columnType) {
case 'home':
case 'notifications':
case 'public':
case 'thread':
return columnType;
default:
if (columnType.indexOf('list:') > -1) {
return 'home';
} else {
return 'public'; // community, account, hashtag
}
}
};
const escapeRegExp = string =>
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
const regexFromFilters = filters => {
if (filters.size === 0) {
return null;
}
return new RegExp(filters.map(filter => escapeRegExp(filter.get('phrase'))).join('|'), 'i');
};
export const makeGetStatus = () => {
return createSelector(
[
(state, id) => state.getIn(['statuses', id]),
(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', state.getIn(['statuses', id, 'reblog']), 'account'])]),
(state, { id }) => state.getIn(['statuses', id]),
(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', state.getIn(['statuses', id, 'reblog']), 'account'])]),
(state, { contextType }) => state.get('filters', ImmutableList()).filter(filter => contextType && filter.get('context').includes(toServerSideType(contextType)) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))),
],
(statusBase, statusReblog, accountBase, accountReblog) => {
(statusBase, statusReblog, accountBase, accountReblog, filters) => {
if (!statusBase) {
return null;
}
@ -39,9 +67,13 @@ export const makeGetStatus = () => {
statusReblog = null;
}
const regex = regexFromFilters(filters);
const filtered = regex && regex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index'));
return statusBase.withMutations(map => {
map.set('reblog', statusReblog);
map.set('account', accountBase);
map.set('filtered', filtered);
});
}
);

View File

@ -111,6 +111,20 @@
}
}
.status__wrapper--filtered {
color: $dark-text-color;
border: 0;
font-size: inherit;
text-align: center;
line-height: inherit;
margin: 0;
padding: 15px;
box-sizing: border-box;
width: 100%;
clear: both;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.status__prepend-icon-wrapper {
float: left;
margin: 0 10px 0 -58px;