diff options
Diffstat (limited to 'app/javascript')
-rw-r--r-- | app/javascript/mastodon/actions/filters.js | 26 | ||||
-rw-r--r-- | app/javascript/mastodon/actions/importer/index.js | 11 | ||||
-rw-r--r-- | app/javascript/mastodon/actions/importer/normalizer.js | 12 | ||||
-rw-r--r-- | app/javascript/mastodon/actions/notifications.js | 13 | ||||
-rw-r--r-- | app/javascript/mastodon/actions/streaming.js | 4 | ||||
-rw-r--r-- | app/javascript/mastodon/components/status.js | 21 | ||||
-rw-r--r-- | app/javascript/mastodon/components/status_action_bar.js | 17 | ||||
-rw-r--r-- | app/javascript/mastodon/features/ui/index.js | 3 | ||||
-rw-r--r-- | app/javascript/mastodon/reducers/filters.js | 34 | ||||
-rw-r--r-- | app/javascript/mastodon/selectors/index.js | 51 | ||||
-rw-r--r-- | app/javascript/packs/public.js | 1 | ||||
-rw-r--r-- | app/javascript/styles/mastodon/admin.scss | 33 | ||||
-rw-r--r-- | app/javascript/styles/mastodon/components.scss | 15 | ||||
-rw-r--r-- | app/javascript/styles/mastodon/forms.scss | 31 |
14 files changed, 191 insertions, 81 deletions
diff --git a/app/javascript/mastodon/actions/filters.js b/app/javascript/mastodon/actions/filters.js deleted file mode 100644 index 7fa1c9a70..000000000 --- a/app/javascript/mastodon/actions/filters.js +++ /dev/null @@ -1,26 +0,0 @@ -import api from '../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/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index f4372fb31..9c69be601 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -5,6 +5,7 @@ export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; export const POLLS_IMPORT = 'POLLS_IMPORT'; +export const FILTERS_IMPORT = 'FILTERS_IMPORT'; function pushUnique(array, object) { if (array.every(element => element.id !== object.id)) { @@ -28,6 +29,10 @@ export function importStatuses(statuses) { return { type: STATUSES_IMPORT, statuses }; } +export function importFilters(filters) { + return { type: FILTERS_IMPORT, filters }; +} + export function importPolls(polls) { return { type: POLLS_IMPORT, polls }; } @@ -61,11 +66,16 @@ export function importFetchedStatuses(statuses) { const accounts = []; const normalStatuses = []; const polls = []; + const filters = []; function processStatus(status) { pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]))); pushUnique(accounts, status.account); + if (status.filtered) { + status.filtered.forEach(result => pushUnique(filters, result.filter)); + } + if (status.reblog && status.reblog.id) { processStatus(status.reblog); } @@ -80,6 +90,7 @@ export function importFetchedStatuses(statuses) { dispatch(importPolls(polls)); dispatch(importFetchedAccounts(accounts)); dispatch(importStatuses(normalStatuses)); + dispatch(importFilters(filters)); }; } diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index ca76e3494..8a22f83fa 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -42,6 +42,14 @@ export function normalizeAccount(account) { return account; } +export function normalizeFilterResult(result) { + const normalResult = { ...result }; + + normalResult.filter = normalResult.filter.id; + + return normalResult; +} + export function normalizeStatus(status, normalOldStatus) { const normalStatus = { ...status }; normalStatus.account = status.account.id; @@ -54,6 +62,10 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.poll = status.poll.id; } + if (status.filtered) { + normalStatus.filtered = status.filtered.map(normalizeFilterResult); + } + // Only calculate these values when status first encountered and // when the underlying values change. Otherwise keep the ones // already in the reducer diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 84dfbeef3..3c42f71da 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -12,10 +12,8 @@ import { saveSettings } from './settings'; import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from '../utils/html'; -import { getFiltersRegex } from '../selectors'; import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; import compareId from 'mastodon/compare_id'; -import { searchTextFromRawStatus } from 'mastodon/actions/importer/normalizer'; import { requestNotificationPermission } from '../utils/notifications'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; @@ -62,20 +60,17 @@ export function updateNotifications(notification, intlMessages, intlLocale) { 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 playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); - const filters = getFiltersRegex(getState(), { contextType: 'notifications' }); let filtered = false; - if (['mention', 'status'].includes(notification.type)) { - const dropRegex = filters[0]; - const regex = filters[1]; - const searchIndex = searchTextFromRawStatus(notification.status); + if (['mention', 'status'].includes(notification.type) && notification.status.filtered) { + const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications')); - if (dropRegex && dropRegex.test(searchIndex)) { + if (filters.some(result => result.filter.filter_action === 'hide')) { return; } - filtered = regex && regex.test(searchIndex); + filtered = filters.length > 0; } if (['follow_request'].includes(notification.type)) { diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index d76f045c8..84709083f 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -21,7 +21,6 @@ import { updateReaction as updateAnnouncementsReaction, deleteAnnouncement, } from './announcements'; -import { fetchFilters } from './filters'; import { getLocale } from '../locales'; const { messages } = getLocale(); @@ -97,9 +96,6 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti case 'conversation': dispatch(updateConversations(JSON.parse(data.payload))); break; - case 'filters_changed': - dispatch(fetchFilters()); - break; case 'announcement': dispatch(updateAnnouncements(JSON.parse(data.payload))); break; diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 7c44669d2..4ca392824 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -116,6 +116,7 @@ class Status extends ImmutablePureComponent { state = { showMedia: defaultMediaVisibility(this.props.status), statusId: undefined, + forceFilter: undefined, }; static getDerivedStateFromProps(nextProps, prevState) { @@ -277,6 +278,15 @@ class Status extends ImmutablePureComponent { this.handleToggleMediaVisibility(); } + handleUnfilterClick = e => { + this.setState({ forceFilter: false }); + e.preventDefault(); + } + + handleFilterClick = () => { + this.setState({ forceFilter: true }); + } + _properStatus () { const { status } = this.props; @@ -328,7 +338,8 @@ class Status extends ImmutablePureComponent { ); } - if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) { + const matchedFilters = status.get('filtered') || status.getIn(['reblog', 'filtered']); + if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) { const minHandlers = this.props.muted ? {} : { moveUp: this.handleHotkeyMoveUp, moveDown: this.handleHotkeyMoveDown, @@ -337,7 +348,11 @@ class Status extends ImmutablePureComponent { return ( <HotKeys handlers={minHandlers}> <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(', ')}. + {' '} + <button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}> + <FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' /> + </button> </div> </HotKeys> ); @@ -496,7 +511,7 @@ class Status extends ImmutablePureComponent { {media} - <StatusActionBar scrollKey={scrollKey} status={status} account={account} {...other} /> + <StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters && this.handleFilterClick} {...other} /> </div> </div> </HotKeys> diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 1d8fe23da..ab8755be0 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -38,6 +38,7 @@ const messages = defineMessages({ admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, + hide: { id: 'status.hide', defaultMessage: 'Hide toot' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, @@ -76,6 +77,7 @@ class StatusActionBar extends ImmutablePureComponent { onMuteConversation: PropTypes.func, onPin: PropTypes.func, onBookmark: PropTypes.func, + onFilter: PropTypes.func, withDismiss: PropTypes.bool, withCounters: PropTypes.bool, scrollKey: PropTypes.string, @@ -207,6 +209,10 @@ class StatusActionBar extends ImmutablePureComponent { this.props.onMuteConversation(this.props.status); } + handleFilter = () => { + this.props.onFilter(); + } + handleCopy = () => { const url = this.props.status.get('url'); const textarea = document.createElement('textarea'); @@ -226,6 +232,11 @@ class StatusActionBar extends ImmutablePureComponent { } } + + handleFilterClick = () => { + this.props.onFilter(); + } + render () { const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; @@ -329,6 +340,10 @@ class StatusActionBar extends ImmutablePureComponent { <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} /> ); + const filterButton = this.props.onFilter && ( + <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleFilterClick} /> + ); + return ( <div className='status__action-bar'> <IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount /> @@ -337,6 +352,8 @@ class StatusActionBar extends ImmutablePureComponent { {shareButton} + {filterButton} + <div className='status__action-bar-dropdown'> <DropdownMenuContainer scrollKey={scrollKey} diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 1ee038223..9a901f12a 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -13,7 +13,6 @@ import { debounce } from 'lodash'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { expandHomeTimeline } from '../../actions/timelines'; import { expandNotifications } from '../../actions/notifications'; -import { fetchFilters } from '../../actions/filters'; import { fetchRules } from '../../actions/rules'; import { clearHeight } from '../../actions/height_cache'; import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app'; @@ -368,7 +367,7 @@ class UI extends React.PureComponent { this.props.dispatch(fetchMarkers()); this.props.dispatch(expandHomeTimeline()); this.props.dispatch(expandNotifications()); - setTimeout(() => this.props.dispatch(fetchFilters()), 500); + setTimeout(() => this.props.dispatch(fetchRules()), 3000); this.hotkeys.__mousetrap__.stopCallback = (e, element) => { diff --git a/app/javascript/mastodon/reducers/filters.js b/app/javascript/mastodon/reducers/filters.js index 33f0c6732..14b704027 100644 --- a/app/javascript/mastodon/reducers/filters.js +++ b/app/javascript/mastodon/reducers/filters.js @@ -1,10 +1,34 @@ -import { FILTERS_FETCH_SUCCESS } from '../actions/filters'; -import { List as ImmutableList, fromJS } from 'immutable'; +import { FILTERS_IMPORT } from '../actions/importer'; +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) { - case FILTERS_FETCH_SUCCESS: - return fromJS(action.filters); + case FILTERS_IMPORT: + return normalizeFilters(state, action.filters); default: return state; } diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index fbd25b605..6aeb8b7bd 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -40,15 +40,15 @@ const toServerSideType = columnType => { const escapeRegExp = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -const regexFromFilters = filters => { - if (filters.size === 0) { +const regexFromKeywords = keywords => { + if (keywords.size === 0) { return null; } - return new RegExp(filters.map(filter => { - let expr = escapeRegExp(filter.get('phrase')); + return new RegExp(keywords.map(keyword_filter => { + let expr = escapeRegExp(keyword_filter.get('keyword')); - if (filter.get('whole_word')) { + if (keyword_filter.get('whole_word')) { if (/^[\w]/.test(expr)) { expr = `\\b${expr}`; } @@ -62,27 +62,15 @@ const regexFromFilters = filters => { }).join('|'), 'i'); }; -// Memoize the filter regexps for each valid server contextType -const makeGetFiltersRegex = () => { - let memo = {}; +const getFilters = (state, { contextType }) => { + if (!contextType) return null; - return (state, { contextType }) => { - if (!contextType) return ImmutableList(); + const serverSideType = toServerSideType(contextType); + const now = new Date(); - const serverSideType = toServerSideType(contextType); - 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; - }; + return state.get('filters').filter((filter) => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || filter.get('expires_at') > now)); }; -export const getFiltersRegex = makeGetFiltersRegex(); - export const makeGetStatus = () => { return createSelector( [ @@ -90,10 +78,10 @@ export const makeGetStatus = () => { (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'])]), - getFiltersRegex, + getFilters, ], - (statusBase, statusReblog, accountBase, accountReblog, filtersRegex) => { + (statusBase, statusReblog, accountBase, accountReblog, filters) => { if (!statusBase) { return null; } @@ -104,14 +92,17 @@ export const makeGetStatus = () => { statusReblog = null; } - const dropRegex = (accountReblog || accountBase).get('id') !== me && filtersRegex[0]; - if (dropRegex && dropRegex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index'))) { - return null; + let filtered = false; + if ((accountReblog || accountBase).get('id') !== me && filters) { + let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList(); + 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]; - 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); diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 3d0a937e1..e42468e0c 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -4,6 +4,7 @@ import loadPolyfills from '../mastodon/load_polyfills'; import ready from '../mastodon/ready'; import { start } from '../mastodon/common'; import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; +import 'cocoon-js-vanilla'; start(); diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 66e2997f1..4ce5cd101 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -915,7 +915,8 @@ a.name-tag, text-align: center; } -.applications-list__item { +.applications-list__item, +.filters-list__item { padding: 15px 0; background: $ui-base-color; border: 1px solid lighten($ui-base-color, 4%); @@ -923,7 +924,8 @@ a.name-tag, margin-top: 15px; } -.announcements-list { +.announcements-list, +.filters-list { border: 1px solid lighten($ui-base-color, 4%); border-radius: 4px; @@ -976,6 +978,33 @@ a.name-tag, } } +.filters-list__item { + &__title { + display: flex; + justify-content: space-between; + margin-bottom: 0; + } + + &__permissions { + margin-top: 0; + margin-bottom: 10px; + } + + .expiration { + font-size: 13px; + } + + &.expired { + .expiration { + color: lighten($error-red, 12%); + } + + .permissions-list__item__icon { + color: $dark-text-color; + } + } +} + .dashboard__counters.admin-account-counters { margin-top: 10px; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 7e3ce3de2..592ce91f3 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -959,6 +959,21 @@ width: 100%; clear: both; border-bottom: 1px solid lighten($ui-base-color, 8%); + + &__button { + display: inline; + color: lighten($ui-highlight-color, 8%); + border: 0; + background: transparent; + padding: 0; + font-size: inherit; + line-height: inherit; + + &:hover, + &:active { + text-decoration: underline; + } + } } .status__prepend-icon-wrapper { diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index d57eabc09..da699dd25 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -1070,3 +1070,34 @@ code { } } } + +.keywords-table { + thead { + th { + white-space: nowrap; + } + + th:first-child { + width: 100%; + } + } + + tfoot { + td { + border: 0; + } + } + + .input.string { + margin-bottom: 0; + } + + .label_input__wrapper { + margin-top: 10px; + } + + .table-action-link { + margin-top: 10px; + white-space: nowrap; + } +} |