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