Add a way to know why a status has been filtered, and show it anyway
This commit is contained in:
		
							parent
							
								
									79e97d71d4
								
							
						
					
					
						commit
						67d1b0c997
					
				
					 6 changed files with 147 additions and 5 deletions
				
			
		| 
						 | 
				
			
			@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue