Add a way to know why a status has been filtered, and show it anyway

This commit is contained in:
Thibaut Girka 2019-07-12 16:01:33 +02:00 committed by ThibG
parent 79e97d71d4
commit 67d1b0c997
6 changed files with 147 additions and 5 deletions

View file

@ -106,6 +106,7 @@ class Status extends ImmutablePureComponent {
statusId: undefined, statusId: undefined,
revealBehindCW: undefined, revealBehindCW: undefined,
showCard: false, showCard: false,
bypassFilter: false,
} }
// Avoid checking props that are functions (and whose equality will always // Avoid checking props that are functions (and whose equality will always
@ -126,6 +127,7 @@ class Status extends ImmutablePureComponent {
'isExpanded', 'isExpanded',
'isCollapsed', 'isCollapsed',
'showMedia', 'showMedia',
'bypassFilter',
] ]
// If our settings have changed to disable collapsed statuses, then we // If our settings have changed to disable collapsed statuses, then we
@ -427,6 +429,15 @@ class Status extends ImmutablePureComponent {
this.handleToggleMediaVisibility(); 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 => { handleRef = c => {
this.node = 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 ? {} : { const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp, moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown, moveDown: this.handleHotkeyMoveDown,
@ -495,6 +506,9 @@ class Status extends ImmutablePureComponent {
<HotKeys handlers={minHandlers}> <HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}> <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' />
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show why' />
</button>
</div> </div>
</HotKeys> </HotKeys>
); );
@ -689,6 +703,7 @@ class Status extends ImmutablePureComponent {
account={status.get('account')} account={status.get('account')}
showReplyCount={settings.get('show_reply_count')} showReplyCount={settings.get('show_reply_count')}
directMessage={!!otherAccounts} directMessage={!!otherAccounts}
onFilter={this.handleFilterClick}
/> />
) : null} ) : null}
{notification ? ( {notification ? (

View file

@ -35,6 +35,7 @@ const messages = defineMessages({
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, 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' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
hide: { id: 'status.hide', defaultMessage: 'Hide toot' },
}); });
const obfuscatedCount = count => { const obfuscatedCount = count => {
@ -69,6 +70,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
onMuteConversation: PropTypes.func, onMuteConversation: PropTypes.func,
onPin: PropTypes.func, onPin: PropTypes.func,
onBookmark: PropTypes.func, onBookmark: PropTypes.func,
onFilter: PropTypes.func,
withDismiss: PropTypes.bool, withDismiss: PropTypes.bool,
showReplyCount: PropTypes.bool, showReplyCount: PropTypes.bool,
directMessage: PropTypes.bool, directMessage: PropTypes.bool,
@ -191,6 +193,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
} }
} }
handleFilterClick = () => {
this.props.onFilter();
}
render () { render () {
const { status, intl, withDismiss, showReplyCount, directMessage } = this.props; 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} /> <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 = ( let replyButton = (
<IconButton <IconButton
className='status__action-bar-button' 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} />, <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, 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} />, <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'> <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)} /> <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
</div>, </div>,

View file

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Status from 'flavours/glitch/components/status'; 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 { import {
replyCompose, replyCompose,
mentionCompose, mentionCompose,
@ -26,6 +27,7 @@ import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state'; import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
import { showAlertForError } from '../actions/alerts'; import { showAlertForError } from '../actions/alerts';
import AccountContainer from 'flavours/glitch/containers/account_container';
const messages = defineMessages({ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@ -36,8 +38,49 @@ const messages = defineMessages({
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, 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?' }, 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' }, 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 makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
@ -69,7 +112,7 @@ const makeMapStateToProps = () => {
return mapStateToProps; return mapStateToProps;
}; };
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
onReply (status, router) { onReply (status, router) {
dispatch((_, getState) => { 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) { onReport (status) {
dispatch(initReport(status.get('account'), status)); dispatch(initReport(status.get('account'), status));
}, },

View file

@ -20,7 +20,7 @@ export const makeGetAccount = () => {
}); });
}; };
const toServerSideType = columnType => { export const toServerSideType = columnType => {
switch (columnType) { switch (columnType) {
case 'home': case 'home':
case 'notifications': case 'notifications':
@ -39,7 +39,7 @@ const toServerSideType = columnType => {
const escapeRegExp = string => const escapeRegExp = string =>
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
const regexFromFilters = filters => { export const regexFromFilters = filters => {
if (filters.size === 0) { if (filters.size === 0) {
return null; return null;
} }

View file

@ -820,3 +820,33 @@
left: 0; 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;
}
}

View file

@ -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;
}
}