statusId: undefined,
revealBehindCW: undefined,
showCard: false,
+ bypassFilter: false,
}
// Avoid checking props that are functions (and whose equality will always
'isExpanded',
'isCollapsed',
'showMedia',
+ 'bypassFilter',
]
// If our settings have changed to disable collapsed statuses, then we
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;
}
);
}
- 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,
<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>
);
account={status.get('account')}
showReplyCount={settings.get('show_reply_count')}
directMessage={!!otherAccounts}
+ onFilter={this.handleFilterClick}
/>
) : null}
{notification ? (
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 => {
onMuteConversation: PropTypes.func,
onPin: PropTypes.func,
onBookmark: PropTypes.func,
+ onFilter: PropTypes.func,
withDismiss: PropTypes.bool,
showReplyCount: PropTypes.bool,
directMessage: PropTypes.bool,
}
}
+ handleFilterClick = () => {
+ this.props.onFilter();
+ }
+
render () {
const { status, intl, withDismiss, showReplyCount, directMessage } = this.props;
<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'
<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>,
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,
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' },
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();
return mapStateToProps;
};
-const mapDispatchToProps = (dispatch, { intl }) => ({
+const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
onReply (status, router) {
dispatch((_, getState) => {
}));
},
+ 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));
},
});
};
-const toServerSideType = columnType => {
+export const toServerSideType = columnType => {
switch (columnType) {
case 'home':
case 'notifications':
const escapeRegExp = string =>
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
-const regexFromFilters = filters => {
+export const regexFromFilters = filters => {
if (filters.size === 0) {
return null;
}
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;
+ }
+}
}
}
}
+
+.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;
+ }
+}