diff --git a/app/javascript/flavours/glitch/actions/filters.js b/app/javascript/flavours/glitch/actions/filters.js new file mode 100644 index 000000000..050b30322 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/filters.js @@ -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, + })); +}; diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index 6e34d0be6..00bbb78ae 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -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; } }, }; diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index a90ff491e..c03c3017e 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -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 ( + +
+ +
+
+ ); + } + // If user backgrounds for collapsed statuses are enabled, then we // initialize our background accordingly. This will only be rendered if // the status is collapsed. diff --git a/app/javascript/flavours/glitch/components/status_list.js b/app/javascript/flavours/glitch/components/status_list.js index 33bc7a959..aa902db47 100644 --- a/app/javascript/flavours/glitch/components/status_list.js +++ b/app/javascript/flavours/glitch/components/status_list.js @@ -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); } diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index feff06860..48cb76f86 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -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; diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index cae6b1acb..333a8b0d2 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -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' /> )); } diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 4a7a7d0f4..a0fe395a2 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -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 () { diff --git a/app/javascript/flavours/glitch/reducers/filters.js b/app/javascript/flavours/glitch/reducers/filters.js new file mode 100644 index 000000000..33f0c6732 --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/filters.js @@ -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; + } +}; diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index dc8cb6834..7b7bc2ca2 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/reducers/index.js @@ -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); diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js index e47ec5183..56eca1f02 100644 --- a/app/javascript/flavours/glitch/selectors/index.js +++ b/app/javascript/flavours/glitch/selectors/index.js @@ -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); }); } ); diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index 48a45fae5..de481f937 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -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;