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:
parent
33c1607c83
commit
0bb1720495
11 changed files with 119 additions and 8 deletions
26
app/javascript/flavours/glitch/actions/filters.js
Normal file
26
app/javascript/flavours/glitch/actions/filters.js
Normal 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,
|
||||||
|
}));
|
||||||
|
};
|
|
@ -6,6 +6,7 @@ import {
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotifications, expandNotifications } from './notifications';
|
||||||
|
import { fetchFilters } from './filters';
|
||||||
import { getLocale } from 'mastodon/locales';
|
import { getLocale } from 'mastodon/locales';
|
||||||
|
|
||||||
const { messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
|
@ -30,6 +31,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
|
||||||
case 'notification':
|
case 'notification':
|
||||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
||||||
break;
|
break;
|
||||||
|
case 'filters_changed':
|
||||||
|
dispatch(fetchFilters());
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,7 @@ import StatusIcons from './status_icons';
|
||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
import StatusActionBar from './status_action_bar';
|
import StatusActionBar from './status_action_bar';
|
||||||
import AttachmentList from './attachment_list';
|
import AttachmentList from './attachment_list';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
|
import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
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
|
// If user backgrounds for collapsed statuses are enabled, then we
|
||||||
// initialize our background accordingly. This will only be rendered if
|
// initialize our background accordingly. This will only be rendered if
|
||||||
// the status is collapsed.
|
// the status is collapsed.
|
||||||
|
|
|
@ -24,6 +24,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
prepend: PropTypes.node,
|
prepend: PropTypes.node,
|
||||||
emptyMessage: PropTypes.node,
|
emptyMessage: PropTypes.node,
|
||||||
|
timelineId: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -69,7 +70,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { statusIds, featuredStatusIds, onLoadMore, ...other } = this.props;
|
const { statusIds, featuredStatusIds, onLoadMore, timelineId, ...other } = this.props;
|
||||||
const { isLoading, isPartial } = other;
|
const { isLoading, isPartial } = other;
|
||||||
|
|
||||||
if (isPartial) {
|
if (isPartial) {
|
||||||
|
@ -101,6 +102,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
id={statusId}
|
id={statusId}
|
||||||
onMoveUp={this.handleMoveUp}
|
onMoveUp={this.handleMoveUp}
|
||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
|
contextType={timelineId}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : null;
|
) : null;
|
||||||
|
@ -113,6 +115,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
featured
|
featured
|
||||||
onMoveUp={this.handleMoveUp}
|
onMoveUp={this.handleMoveUp}
|
||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
|
contextType={timelineId}
|
||||||
/>
|
/>
|
||||||
)).concat(scrollableContent);
|
)).concat(scrollableContent);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ const makeMapStateToProps = () => {
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = (state, props) => {
|
||||||
|
|
||||||
let status = getStatus(state, props.id);
|
let status = getStatus(state, props);
|
||||||
let reblogStatus = status ? status.get('reblog', null) : null;
|
let reblogStatus = status ? status.get('reblog', null) : null;
|
||||||
let account = undefined;
|
let account = undefined;
|
||||||
let prepend = undefined;
|
let prepend = undefined;
|
||||||
|
|
|
@ -53,7 +53,7 @@ const makeMapStateToProps = () => {
|
||||||
const getStatus = makeGetStatus();
|
const getStatus = makeGetStatus();
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
status: getStatus(state, props.params.statusId),
|
status: getStatus(state, { id: props.params.statusId }),
|
||||||
settings: state.get('local_settings'),
|
settings: state.get('local_settings'),
|
||||||
ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
|
ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
|
||||||
descendantsIds: state.getIn(['contexts', 'descendants', 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}
|
expanded={this.state.threadExpanded}
|
||||||
onMoveUp={this.handleMoveUp}
|
onMoveUp={this.handleMoveUp}
|
||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
|
contextType='thread'
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { debounce } from 'lodash';
|
||||||
import { uploadCompose, resetCompose } from 'flavours/glitch/actions/compose';
|
import { uploadCompose, resetCompose } from 'flavours/glitch/actions/compose';
|
||||||
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
|
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
import { expandNotifications } from 'flavours/glitch/actions/notifications';
|
import { expandNotifications } from 'flavours/glitch/actions/notifications';
|
||||||
|
import { fetchFilters } from 'flavours/glitch/actions/filters';
|
||||||
import { clearHeight } from 'flavours/glitch/actions/height_cache';
|
import { clearHeight } from 'flavours/glitch/actions/height_cache';
|
||||||
import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers';
|
import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers';
|
||||||
import UploadArea from './components/upload_area';
|
import UploadArea from './components/upload_area';
|
||||||
|
@ -218,6 +219,7 @@ export default class UI extends React.Component {
|
||||||
|
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
|
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
|
11
app/javascript/flavours/glitch/reducers/filters.js
Normal file
11
app/javascript/flavours/glitch/reducers/filters.js
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -27,6 +27,7 @@ import height_cache from './height_cache';
|
||||||
import custom_emojis from './custom_emojis';
|
import custom_emojis from './custom_emojis';
|
||||||
import lists from './lists';
|
import lists from './lists';
|
||||||
import listEditor from './list_editor';
|
import listEditor from './list_editor';
|
||||||
|
import filters from './filters';
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
dropdown_menu,
|
dropdown_menu,
|
||||||
|
@ -57,6 +58,7 @@ const reducers = {
|
||||||
custom_emojis,
|
custom_emojis,
|
||||||
lists,
|
lists,
|
||||||
listEditor,
|
listEditor,
|
||||||
|
filters,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default combineReducers(reducers);
|
export default combineReducers(reducers);
|
||||||
|
|
|
@ -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 = () => {
|
export const makeGetStatus = () => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
[
|
[
|
||||||
(state, id) => state.getIn(['statuses', id]),
|
(state, { id }) => state.getIn(['statuses', id]),
|
||||||
(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, { 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) {
|
if (!statusBase) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -39,9 +67,13 @@ export const makeGetStatus = () => {
|
||||||
statusReblog = null;
|
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 => {
|
return statusBase.withMutations(map => {
|
||||||
map.set('reblog', statusReblog);
|
map.set('reblog', statusReblog);
|
||||||
map.set('account', accountBase);
|
map.set('account', accountBase);
|
||||||
|
map.set('filtered', filtered);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 {
|
.status__prepend-icon-wrapper {
|
||||||
float: left;
|
float: left;
|
||||||
margin: 0 10px 0 -58px;
|
margin: 0 10px 0 -58px;
|
||||||
|
|
Loading…
Reference in a new issue