forked from treehouse/mastodon
Add a way to know why a status has been filtered, and show it anyway
parent
e9fac2def9
commit
bde7a415b9
|
@ -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 ? (
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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));
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 New Issue