~cytrogen/masto-fe

bde7a415b9ecbe9bcdf5d32918fd2cfcf5dad0d7 — Thibaut Girka 6 years ago e9fac2d
Add a way to know why a status has been filtered, and show it anyway
M app/javascript/flavours/glitch/components/status.js => app/javascript/flavours/glitch/components/status.js +16 -1
@@ 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 ? (

M app/javascript/flavours/glitch/components/status_action_bar.js => app/javascript/flavours/glitch/components/status_action_bar.js +11 -0
@@ 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>,

M app/javascript/flavours/glitch/containers/status_container.js => app/javascript/flavours/glitch/containers/status_container.js +72 -2
@@ 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));
  },

M app/javascript/flavours/glitch/selectors/index.js => app/javascript/flavours/glitch/selectors/index.js +2 -2
@@ 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;
  }

M app/javascript/flavours/glitch/styles/components/modal.scss => app/javascript/flavours/glitch/styles/components/modal.scss +30 -0
@@ 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;
  }
}

M app/javascript/flavours/glitch/styles/components/status.scss => app/javascript/flavours/glitch/styles/components/status.scss +16 -0
@@ 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;
  }
}