Add a way to know why a status has been filtered, and show it anyway
This commit is contained in:
		| @@ -106,6 +106,7 @@ class Status extends ImmutablePureComponent { | ||||
|     statusId: undefined, | ||||
|     revealBehindCW: undefined, | ||||
|     showCard: false, | ||||
|     bypassFilter: false, | ||||
|   } | ||||
|  | ||||
|   // Avoid checking props that are functions (and whose equality will always | ||||
| @@ -126,6 +127,7 @@ class Status extends ImmutablePureComponent { | ||||
|     'isExpanded', | ||||
|     'isCollapsed', | ||||
|     'showMedia', | ||||
|     'bypassFilter', | ||||
|   ] | ||||
|  | ||||
|   //  If our settings have changed to disable collapsed statuses, then we | ||||
| @@ -427,6 +429,15 @@ class Status extends ImmutablePureComponent { | ||||
|     this.handleToggleMediaVisibility(); | ||||
|   } | ||||
|  | ||||
|   handleUnfilterClick = e => { | ||||
|     const { onUnfilter, status } = this.props; | ||||
|     onUnfilter(status.get('reblog') ? status.get('reblog') : status, () => this.setState({ bypassFilter: true })); | ||||
|   } | ||||
|  | ||||
|   handleFilterClick = () => { | ||||
|     this.setState({ bypassFilter: false }); | ||||
|   } | ||||
|  | ||||
|   handleRef = c => { | ||||
|     this.node = c; | ||||
|   } | ||||
| @@ -485,7 +496,7 @@ class Status extends ImmutablePureComponent { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) { | ||||
|     if ((status.get('filtered') || status.getIn(['reblog', 'filtered'])) && !this.state.bypassFilter) { | ||||
|       const minHandlers = this.props.muted ? {} : { | ||||
|         moveUp: this.handleHotkeyMoveUp, | ||||
|         moveDown: this.handleHotkeyMoveDown, | ||||
| @@ -495,6 +506,9 @@ class Status extends ImmutablePureComponent { | ||||
|         <HotKeys handlers={minHandlers}> | ||||
|           <div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}> | ||||
|             <FormattedMessage id='status.filtered' defaultMessage='Filtered' /> | ||||
|             <button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}> | ||||
|               <FormattedMessage id='status.show_filter_reason' defaultMessage='Show why' /> | ||||
|             </button> | ||||
|           </div> | ||||
|         </HotKeys> | ||||
|       ); | ||||
| @@ -689,6 +703,7 @@ class Status extends ImmutablePureComponent { | ||||
|               account={status.get('account')} | ||||
|               showReplyCount={settings.get('show_reply_count')} | ||||
|               directMessage={!!otherAccounts} | ||||
|               onFilter={this.handleFilterClick} | ||||
|             /> | ||||
|           ) : null} | ||||
|           {notification ? ( | ||||
|   | ||||
| @@ -35,6 +35,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' }, | ||||
| }); | ||||
|  | ||||
| const obfuscatedCount = count => { | ||||
| @@ -69,6 +70,7 @@ export default class StatusActionBar extends ImmutablePureComponent { | ||||
|     onMuteConversation: PropTypes.func, | ||||
|     onPin: PropTypes.func, | ||||
|     onBookmark: PropTypes.func, | ||||
|     onFilter: PropTypes.func, | ||||
|     withDismiss: PropTypes.bool, | ||||
|     showReplyCount: PropTypes.bool, | ||||
|     directMessage: PropTypes.bool, | ||||
| @@ -191,6 +193,10 @@ export default class StatusActionBar extends ImmutablePureComponent { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   handleFilterClick = () => { | ||||
|     this.props.onFilter(); | ||||
|   } | ||||
|  | ||||
|   render () { | ||||
|     const { status, intl, withDismiss, showReplyCount, directMessage } = this.props; | ||||
|  | ||||
| @@ -263,6 +269,10 @@ export default class StatusActionBar extends ImmutablePureComponent { | ||||
|       <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} /> | ||||
|     ); | ||||
|  | ||||
|     const filterButton = status.get('filtered') && ( | ||||
|       <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleFilterClick} /> | ||||
|     ); | ||||
|  | ||||
|     let replyButton = ( | ||||
|       <IconButton | ||||
|         className='status__action-bar-button' | ||||
| @@ -288,6 +298,7 @@ export default class StatusActionBar extends ImmutablePureComponent { | ||||
|           <IconButton key='favourite-button' className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />, | ||||
|           shareButton, | ||||
|           <IconButton key='bookmark-button' className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />, | ||||
|           filterButton, | ||||
|           <div key='dropdown-button' className='status__action-bar-dropdown'> | ||||
|             <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} /> | ||||
|           </div>, | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import React from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import Status from 'flavours/glitch/components/status'; | ||||
| import { makeGetStatus } from 'flavours/glitch/selectors'; | ||||
| import { List as ImmutableList } from 'immutable'; | ||||
| import { makeGetStatus, regexFromFilters, toServerSideType } from 'flavours/glitch/selectors'; | ||||
| import { | ||||
|   replyCompose, | ||||
|   mentionCompose, | ||||
| @@ -26,6 +27,7 @@ import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; | ||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
| import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state'; | ||||
| import { showAlertForError } from '../actions/alerts'; | ||||
| import AccountContainer from 'flavours/glitch/containers/account_container'; | ||||
|  | ||||
| const messages = defineMessages({ | ||||
|   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, | ||||
| @@ -36,8 +38,49 @@ const messages = defineMessages({ | ||||
|   replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, | ||||
|   replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, | ||||
|   blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, | ||||
|   unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' }, | ||||
| }); | ||||
|  | ||||
| class SpoilerMachin extends React.PureComponent { | ||||
|   state = { | ||||
|     hidden: true, | ||||
|   } | ||||
|  | ||||
|   handleSpoilerClick = () => { | ||||
|     this.setState({ hidden: !this.state.hidden }); | ||||
|   } | ||||
|  | ||||
|   render () { | ||||
|     const { spoilerText, children } = this.props; | ||||
|     const { hidden } = this.state; | ||||
|  | ||||
|       const toggleText = hidden ? | ||||
|         <FormattedMessage | ||||
|           id='status.show_more' | ||||
|           defaultMessage='Show more' | ||||
|           key='0' | ||||
|         /> : | ||||
|         <FormattedMessage | ||||
|           id='status.show_less' | ||||
|           defaultMessage='Show less' | ||||
|           key='0' | ||||
|         />; | ||||
|  | ||||
|     return ([ | ||||
|       <p className='spoiler__text'> | ||||
|         {spoilerText} | ||||
|         {' '} | ||||
|         <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}> | ||||
|           {toggleText} | ||||
|         </button> | ||||
|       </p>, | ||||
|       <div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}> | ||||
|         {children} | ||||
|       </div> | ||||
|     ]); | ||||
|   } | ||||
| } | ||||
|  | ||||
| const makeMapStateToProps = () => { | ||||
|   const getStatus = makeGetStatus(); | ||||
|  | ||||
| @@ -69,7 +112,7 @@ const makeMapStateToProps = () => { | ||||
|   return mapStateToProps; | ||||
| }; | ||||
|  | ||||
| const mapDispatchToProps = (dispatch, { intl }) => ({ | ||||
| const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ | ||||
|  | ||||
|   onReply (status, router) { | ||||
|     dispatch((_, getState) => { | ||||
| @@ -189,6 +232,33 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ | ||||
|     })); | ||||
|   }, | ||||
|  | ||||
|   onUnfilter (status, onConfirm) { | ||||
|     dispatch((_, getState) => { | ||||
|       let state = getState(); | ||||
|       const serverSideType = toServerSideType(contextType); | ||||
|       const enabledFilters = state.get('filters', ImmutableList()).filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))).toArray(); | ||||
|       const searchIndex = status.get('search_index'); | ||||
|       const matchingFilters = enabledFilters.filter(filter => regexFromFilters([filter]).test(searchIndex)); | ||||
|       dispatch(openModal('CONFIRM', { | ||||
|         message: [ | ||||
|           <FormattedMessage id='confirmations.unfilter' defaultMessage='Information about this filtered toot' />, | ||||
|           <div className='filtered-status-info'> | ||||
|             <SpoilerMachin spoilerText='Author'> | ||||
|               <AccountContainer id={status.getIn(['account', 'id'])} /> | ||||
|             </SpoilerMachin> | ||||
|             <SpoilerMachin spoilerText='Matching filters'> | ||||
|               <ul> | ||||
|                 {matchingFilters.map(filter => <li>{filter.get('phrase')}</li>)} | ||||
|               </ul> | ||||
|             </SpoilerMachin> | ||||
|           </div> | ||||
|         ], | ||||
|         confirm: intl.formatMessage(messages.unfilterConfirm), | ||||
|         onConfirm: onConfirm, | ||||
|       })); | ||||
|     }); | ||||
|   }, | ||||
|  | ||||
|   onReport (status) { | ||||
|     dispatch(initReport(status.get('account'), status)); | ||||
|   }, | ||||
|   | ||||
| @@ -20,7 +20,7 @@ export const makeGetAccount = () => { | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| const toServerSideType = columnType => { | ||||
| export const toServerSideType = columnType => { | ||||
|   switch (columnType) { | ||||
|   case 'home': | ||||
|   case 'notifications': | ||||
| @@ -39,7 +39,7 @@ const toServerSideType = columnType => { | ||||
| const escapeRegExp = string => | ||||
|   string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string | ||||
|  | ||||
| const regexFromFilters = filters => { | ||||
| export const regexFromFilters = filters => { | ||||
|   if (filters.size === 0) { | ||||
|     return null; | ||||
|   } | ||||
|   | ||||
| @@ -820,3 +820,33 @@ | ||||
|     left: 0; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .filtered-status-info { | ||||
|   text-align: start; | ||||
|  | ||||
|   .spoiler__text { | ||||
|     margin-top: 20px; | ||||
|   } | ||||
|  | ||||
|   .account { | ||||
|     border-bottom: 0; | ||||
|   } | ||||
|  | ||||
|   .account__display-name strong { | ||||
|     color: $inverted-text-color; | ||||
|   } | ||||
|  | ||||
|   .status__content__spoiler { | ||||
|     display: none; | ||||
|  | ||||
|     &--visible { | ||||
|       display: flex; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ul { | ||||
|     padding: 10px; | ||||
|     margin-left: 12px; | ||||
|     list-style: disc inside; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -996,3 +996,19 @@ a.status-card.compact:hover { | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .status__wrapper--filtered__button { | ||||
|   display: block; | ||||
|   font-size: 15px; | ||||
|   line-height: 20px; | ||||
|   color: lighten($ui-highlight-color, 8%); | ||||
|   border: 0; | ||||
|   background: transparent; | ||||
|   padding: 0; | ||||
|   padding-top: 8px; | ||||
|  | ||||
|   &:hover, | ||||
|   &:active { | ||||
|     text-decoration: underline; | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user