diff options
author | Claire <claire.github-309c@sitedethib.com> | 2022-06-28 09:42:13 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-06-28 09:42:13 +0200 |
commit | 02851848e964675bb59919fa5fd1bdee2c1c29db (patch) | |
tree | 55f0836a4dda9f8de3a9c9511d59e5b726fd77a7 /app/javascript/mastodon/components | |
parent | 5823ae70c4c7c297c8d69ecd0be8df65019411e3 (diff) |
Revamp post filtering system (#18058)
* Add model for custom filter keywords * Use CustomFilterKeyword internally Does not change the API * Fix /filters/edit and /filters/new * Add migration tests * Remove whole_word column from custom_filters (covered by custom_filter_keywords) * Redesign /filters Instead of a list, present a card that displays more information and handles multiple keywords per filter. * Redesign /filters/new and /filters/edit to add and remove keywords This adds a new gem dependency: cocoon, as well as a npm dependency: cocoon-js-vanilla. Those are used to easily populate and remove form fields from the user interface when manipulating multiple keyword filters at once. * Add /api/v2/filters to edit filter with multiple keywords Entities: - `Filter`: `id`, `title`, `filter_action` (either `hide` or `warn`), `context` `keywords` - `FilterKeyword`: `id`, `keyword`, `whole_word` API endpoits: - `GET /api/v2/filters` to list filters (including keywords) - `POST /api/v2/filters` to create a new filter `keywords_attributes` can also be passed to create keywords in one request - `GET /api/v2/filters/:id` to read a particular filter - `PUT /api/v2/filters/:id` to update a new filter `keywords_attributes` can also be passed to edit, delete or add keywords in one request - `DELETE /api/v2/filters/:id` to delete a particular filter - `GET /api/v2/filters/:id/keywords` to list keywords for a filter - `POST /api/v2/filters/:filter_id/keywords/:id` to add a new keyword to a filter - `GET /api/v2/filter_keywords/:id` to read a particular keyword - `PUT /api/v2/filter_keywords/:id` to edit a particular keyword - `DELETE /api/v2/filter_keywords/:id` to delete a particular keyword * Change from `irreversible` boolean to `action` enum * Remove irrelevent `irreversible_must_be_within_context` check * Fix /filters/new and /filters/edit with update for filter_action * Fix Rubocop/Codeclimate complaining about task names * Refactor FeedManager#phrase_filtered? This moves regexp building and filter caching to the `CustomFilter` class. This does not change the functional behavior yet, but this changes how the cache is built, doing per-custom_filter regexps so that filters can be matched independently, while still offering caching. * Perform server-side filtering and output result in REST API * Fix numerous filters_changed events being sent when editing multiple keywords at once * Add some tests * Use the new API in the WebUI - use client-side logic for filters we have fetched rules for. This is so that filter changes can be retroactively applied without reloading the UI. - use server-side logic for filters we haven't fetched rules for yet (e.g. network error, or initial timeline loading) * Minor optimizations and refactoring * Perform server-side filtering on the streaming server * Change the wording of filter action labels * Fix issues pointed out by linter * Change design of “Show anyway” link in accordence to review comments * Drop “irreversible” filtering behavior * Move /api/v2/filter_keywords to /api/v1/filters/keywords * Rename `filter_results` attribute to `filtered` * Rename REST::LegacyFilterSerializer to REST::V1::FilterSerializer * Fix systemChannelId value in streaming server * Simplify code by removing client-side filtering code The simplifcation comes at a cost though: filters aren't retroactively applied anymore.
Diffstat (limited to 'app/javascript/mastodon/components')
-rw-r--r-- | app/javascript/mastodon/components/status.js | 21 | ||||
-rw-r--r-- | app/javascript/mastodon/components/status_action_bar.js | 17 |
2 files changed, 35 insertions, 3 deletions
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} |