Skip to content

Commit

Permalink
Change how content warnings and filters are displayed in web UI (mast…
Browse files Browse the repository at this point in the history
  • Loading branch information
Gargron authored Aug 22, 2024
1 parent 9823720 commit 500f492
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 141 deletions.
24 changes: 24 additions & 0 deletions app/javascript/images/filter-stripes.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions app/javascript/mastodon/components/content_warning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { StatusBanner, BannerVariant } from './status_banner';

export const ContentWarning: React.FC<{
text: string;
expanded?: boolean;
onClick?: () => void;
}> = ({ text, expanded, onClick }) => (
<StatusBanner
expanded={expanded}
onClick={onClick}
variant={BannerVariant.Yellow}
>
<p dangerouslySetInnerHTML={{ __html: text }} />
</StatusBanner>
);
23 changes: 23 additions & 0 deletions app/javascript/mastodon/components/filter_warning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FormattedMessage } from 'react-intl';

import { StatusBanner, BannerVariant } from './status_banner';

export const FilterWarning: React.FC<{
title: string;
expanded?: boolean;
onClick?: () => void;
}> = ({ title, expanded, onClick }) => (
<StatusBanner
expanded={expanded}
onClick={onClick}
variant={BannerVariant.Blue}
>
<p>
<FormattedMessage
id='filter_warning.matches_filter'
defaultMessage='Matches filter “{title}”'
values={{ title }}
/>
</p>
</StatusBanner>
);
86 changes: 43 additions & 43 deletions app/javascript/mastodon/components/status.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import { ContentWarning } from 'mastodon/components/content_warning';
import { FilterWarning } from 'mastodon/components/filter_warning';
import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'mastodon/utils/react_router';
Expand Down Expand Up @@ -140,7 +142,7 @@ class Status extends ImmutablePureComponent {

state = {
showMedia: defaultMediaVisibility(this.props.status) && !(this.context?.hideMediaByDefault),
forceFilter: undefined,
showDespiteFilter: undefined,
};

componentDidUpdate (prevProps) {
Expand All @@ -152,7 +154,7 @@ class Status extends ImmutablePureComponent {
if (this.props.status?.get('id') !== prevProps.status?.get('id')) {
this.setState({
showMedia: defaultMediaVisibility(this.props.status) && !(this.context?.hideMediaByDefault),
forceFilter: undefined,
showDespiteFilter: undefined,
});
}
}
Expand Down Expand Up @@ -325,20 +327,32 @@ class Status extends ImmutablePureComponent {
};

handleHotkeyToggleHidden = () => {
this.props.onToggleHidden(this._properStatus());
const { onToggleHidden } = this.props;
const status = this._properStatus();

if (status.get('matched_filters')) {
const expandedBecauseOfCW = !status.get('hidden') || status.get('spoiler_text').length === 0;
const expandedBecauseOfFilter = this.state.showDespiteFilter;

if (expandedBecauseOfFilter && !expandedBecauseOfCW) {
onToggleHidden(status);
} else if (expandedBecauseOfFilter && expandedBecauseOfCW) {
onToggleHidden(status);
this.handleFilterToggle();
} else {
this.handleFilterToggle();
}
} else {
onToggleHidden(status);
}
};

handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
};

handleUnfilterClick = e => {
this.setState({ forceFilter: false });
e.preventDefault();
};

handleFilterClick = () => {
this.setState({ forceFilter: true });
handleFilterToggle = () => {
this.setState(state => ({ ...state, showDespiteFilter: !state.showDespiteFilter }));
};

_properStatus () {
Expand Down Expand Up @@ -396,25 +410,6 @@ class Status extends ImmutablePureComponent {
const connectReply = nextInReplyToId && nextInReplyToId === status.get('id');
const matchedFilters = status.get('matched_filters');

if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
};

return (
<HotKeys handlers={minHandlers} tabIndex={unfocusable ? null : -1}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={unfocusable ? null : 0} ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
{' '}
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
</button>
</div>
</HotKeys>
);
}

if (featured) {
prepend = (
<div className='status__prepend'>
Expand Down Expand Up @@ -548,7 +543,7 @@ class Status extends ImmutablePureComponent {
}

const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
const expanded = (!matchedFilters || this.state.showDespiteFilter) && (!status.get('hidden') || status.get('spoiler_text').length === 0);

return (
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
Expand All @@ -574,22 +569,27 @@ class Status extends ImmutablePureComponent {
</a>
</div>

<StatusContent
status={status}
onClick={this.handleClick}
expanded={expanded}
onExpandedToggle={this.handleExpandedToggle}
onTranslate={this.handleTranslate}
collapsible
onCollapsedToggle={this.handleCollapsedToggle}
{...statusContentProps}
/>
{matchedFilters && <FilterWarning title={matchedFilters.join(', ')} expanded={this.state.showDespiteFilter} onClick={this.handleFilterToggle} />}

{media}
{(status.get('spoiler_text').length > 0 && (!matchedFilters || this.state.showDespiteFilter)) && <ContentWarning text={status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml')} expanded={expanded} onClick={this.handleExpandedToggle} />}

{expanded && hashtagBar}
{expanded && (
<>
<StatusContent
status={status}
onClick={this.handleClick}
onTranslate={this.handleTranslate}
collapsible
onCollapsedToggle={this.handleCollapsedToggle}
{...statusContentProps}
/>

{media}
{hashtagBar}
</>
)}

<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
<StatusActionBar scrollKey={scrollKey} status={status} account={account} {...other} />
</div>
</div>
</HotKeys>
Expand Down
12 changes: 0 additions & 12 deletions app/javascript/mastodon/components/status_action_bar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
Expand Down Expand Up @@ -61,7 +60,6 @@ const messages = defineMessages({
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
hide: { id: 'status.hide', defaultMessage: 'Hide post' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
Expand Down Expand Up @@ -241,10 +239,6 @@ class StatusActionBar extends ImmutablePureComponent {
navigator.clipboard.writeText(url);
};

handleHideClick = () => {
this.props.onFilter();
};

render () {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.props.identity;
Expand Down Expand Up @@ -377,10 +371,6 @@ class StatusActionBar extends ImmutablePureComponent {
reblogIconComponent = RepeatDisabledIcon;
}

const filterButton = this.props.onFilter && (
<IconButton className='status__action-bar__button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
);

const isReply = status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);

return (
Expand All @@ -390,8 +380,6 @@ class StatusActionBar extends ImmutablePureComponent {
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />

{filterButton}

<DropdownMenuContainer
scrollKey={scrollKey}
status={status}
Expand Down
37 changes: 37 additions & 0 deletions app/javascript/mastodon/components/status_banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { FormattedMessage } from 'react-intl';

export enum BannerVariant {
Yellow = 'yellow',
Blue = 'blue',
}

export const StatusBanner: React.FC<{
children: React.ReactNode;
variant: BannerVariant;
expanded?: boolean;
onClick?: () => void;
}> = ({ children, variant, expanded, onClick }) => (
<div
className={
variant === BannerVariant.Yellow
? 'content-warning'
: 'content-warning content-warning--filter'
}
>
{children}

<button className='link-button' onClick={onClick}>
{expanded ? (
<FormattedMessage
id='content_warning.hide'
defaultMessage='Hide post'
/>
) : (
<FormattedMessage
id='content_warning.show'
defaultMessage='Show anyway'
/>
)}
</button>
</div>
);
56 changes: 2 additions & 54 deletions app/javascript/mastodon/components/status_content.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PureComponent } from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';

import classnames from 'classnames';
import { Link, withRouter } from 'react-router-dom';
import { withRouter } from 'react-router-dom';

import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
Expand All @@ -15,7 +15,6 @@ import PollContainer from 'mastodon/containers/poll_container';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';


const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)

/**
Expand Down Expand Up @@ -73,8 +72,6 @@ class StatusContent extends PureComponent {
identity: identityContextPropShape,
status: ImmutablePropTypes.map.isRequired,
statusContent: PropTypes.string,
expanded: PropTypes.bool,
onExpandedToggle: PropTypes.func,
onTranslate: PropTypes.func,
onClick: PropTypes.func,
collapsible: PropTypes.bool,
Expand All @@ -87,10 +84,6 @@ class StatusContent extends PureComponent {
history: PropTypes.object.isRequired
};

state = {
hidden: true,
};

_updateStatusLinks () {
const node = this.node;

Expand Down Expand Up @@ -218,17 +211,6 @@ class StatusContent extends PureComponent {
this.startXY = null;
};

handleSpoilerClick = (e) => {
e.preventDefault();

if (this.props.onExpandedToggle) {
// The parent manages the state
this.props.onExpandedToggle();
} else {
this.setState({ hidden: !this.state.hidden });
}
};

handleTranslate = () => {
this.props.onTranslate();
};
Expand All @@ -240,18 +222,15 @@ class StatusContent extends PureComponent {
render () {
const { status, intl, statusContent } = this.props;

const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed');
const contentLocale = intl.locale.replace(/[_-].*/, '');
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);

const content = { __html: statusContent ?? getStatusContent(status) };
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.props.history,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
'status__content--collapsed': renderReadMore,
});

Expand All @@ -269,38 +248,7 @@ class StatusContent extends PureComponent {
<PollContainer pollId={status.get('poll')} lang={language} />
);

if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = '';

const mentionLinks = status.get('mentions').map(item => (
<Link to={`/@${item.get('acct')}`} key={item.get('id')} className='status-link mention'>
@<span>{item.get('username')}</span>
</Link>
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);

const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />;

if (hidden) {
mentionsPlaceholder = <div>{mentionLinks}</div>;
}

return (
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={language} />
{' '}
<button type='button' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick} aria-expanded={!hidden}>{toggleText}</button>
</p>

{mentionsPlaceholder}

<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''} translate`} lang={language} dangerouslySetInnerHTML={content} />

{!hidden && poll}
{translateButton}
</div>
);
} else if (this.props.onClick) {
if (this.props.onClick) {
return (
<>
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
Expand Down
Loading

0 comments on commit 500f492

Please sign in to comment.