Add new filter action to blur media (#34256)

This commit is contained in:
Claire 2025-03-26 08:31:05 +01:00 committed by GitHub
parent 2a181f56e3
commit c93b2c6809
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 46 additions and 11 deletions

View File

@ -226,6 +226,7 @@ class MediaGallery extends PureComponent {
visible: PropTypes.bool, visible: PropTypes.bool,
autoplay: PropTypes.bool, autoplay: PropTypes.bool,
onToggleVisibility: PropTypes.func, onToggleVisibility: PropTypes.func,
matchedFilters: PropTypes.arrayOf(PropTypes.string),
}; };
state = { state = {
@ -296,7 +297,7 @@ class MediaGallery extends PureComponent {
} }
render () { render () {
const { media, lang, sensitive, defaultWidth, autoplay } = this.props; const { media, lang, sensitive, defaultWidth, autoplay, matchedFilters } = this.props;
const { visible } = this.state; const { visible } = this.state;
const width = this.state.width || defaultWidth; const width = this.state.width || defaultWidth;
@ -323,7 +324,7 @@ class MediaGallery extends PureComponent {
<div className={`media-gallery media-gallery--layout-${size}`} style={style} ref={this.handleRef}> <div className={`media-gallery media-gallery--layout-${size}`} style={style} ref={this.handleRef}>
{children} {children}
{(!visible || uncached) && <SpoilerButton uncached={uncached} sensitive={sensitive} onClick={this.handleOpen} />} {(!visible || uncached) && <SpoilerButton uncached={uncached} sensitive={sensitive} onClick={this.handleOpen} matchedFilters={matchedFilters} />}
{(visible && !uncached) && ( {(visible && !uncached) && (
<div className='media-gallery__actions'> <div className='media-gallery__actions'>

View File

@ -6,6 +6,7 @@ interface Props {
hidden?: boolean; hidden?: boolean;
sensitive: boolean; sensitive: boolean;
uncached?: boolean; uncached?: boolean;
matchedFilters?: string[];
onClick: React.MouseEventHandler<HTMLButtonElement>; onClick: React.MouseEventHandler<HTMLButtonElement>;
} }
@ -13,6 +14,7 @@ export const SpoilerButton: React.FC<Props> = ({
hidden = false, hidden = false,
sensitive, sensitive,
uncached = false, uncached = false,
matchedFilters,
onClick, onClick,
}) => { }) => {
let warning; let warning;
@ -28,6 +30,20 @@ export const SpoilerButton: React.FC<Props> = ({
action = ( action = (
<FormattedMessage id='status.media.open' defaultMessage='Click to open' /> <FormattedMessage id='status.media.open' defaultMessage='Click to open' />
); );
} else if (matchedFilters) {
warning = (
<FormattedMessage
id='filter_warning.matches_filter'
defaultMessage='Matches filter “<span>{title}</span>”'
values={{
title: matchedFilters.join(', '),
span: (chunks) => <span className='filter-name'>{chunks}</span>,
}}
/>
);
action = (
<FormattedMessage id='status.media.show' defaultMessage='Click to show' />
);
} else if (sensitive) { } else if (sensitive) {
warning = ( warning = (
<FormattedMessage <FormattedMessage

View File

@ -70,7 +70,7 @@ export const defaultMediaVisibility = (status) => {
status = status.get('reblog'); status = status.get('reblog');
} }
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all'); return !status.get('matched_media_filters') && (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
}; };
const messages = defineMessages({ const messages = defineMessages({
@ -470,6 +470,7 @@ class Status extends ImmutablePureComponent {
defaultWidth={this.props.cachedMediaWidth} defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia} visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
matchedFilters={status.get('matched_media_filters')}
/> />
)} )}
</Bundle> </Bundle>
@ -498,6 +499,7 @@ class Status extends ImmutablePureComponent {
blurhash={attachment.get('blurhash')} blurhash={attachment.get('blurhash')}
visible={this.state.showMedia} visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
matchedFilters={status.get('matched_media_filters')}
/> />
)} )}
</Bundle> </Bundle>
@ -522,6 +524,7 @@ class Status extends ImmutablePureComponent {
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined} deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
visible={this.state.showMedia} visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
matchedFilters={status.get('matched_media_filters')}
/> />
)} )}
</Bundle> </Bundle>

View File

@ -62,6 +62,7 @@ class Audio extends PureComponent {
volume: PropTypes.number, volume: PropTypes.number,
muted: PropTypes.bool, muted: PropTypes.bool,
deployPictureInPicture: PropTypes.func, deployPictureInPicture: PropTypes.func,
matchedFilters: PropTypes.arrayOf(PropTypes.string),
}; };
state = { state = {
@ -472,7 +473,7 @@ class Audio extends PureComponent {
}; };
render () { render () {
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props; const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash, matchedFilters } = this.props;
const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state; const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
const progress = Math.min((currentTime / duration) * 100, 100); const progress = Math.min((currentTime / duration) * 100, 100);
const muted = this.state.muted || volume === 0; const muted = this.state.muted || volume === 0;
@ -514,7 +515,7 @@ class Audio extends PureComponent {
lang={lang} lang={lang}
/> />
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} /> <SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} matchedFilters={matchedFilters} />
{(revealed || editable) && <img {(revealed || editable) && <img
src={this.props.poster} src={this.props.poster}

View File

@ -175,6 +175,7 @@ export const DetailedStatus: React.FC<{
onOpenMedia={onOpenMedia} onOpenMedia={onOpenMedia}
visible={showMedia} visible={showMedia}
onToggleVisibility={onToggleMediaVisibility} onToggleVisibility={onToggleMediaVisibility}
matchedFilters={status.get('matched_media_filters')}
/> />
); );
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
@ -201,6 +202,7 @@ export const DetailedStatus: React.FC<{
blurhash={attachment.get('blurhash')} blurhash={attachment.get('blurhash')}
height={150} height={150}
onToggleVisibility={onToggleMediaVisibility} onToggleVisibility={onToggleMediaVisibility}
matchedFilters={status.get('matched_media_filters')}
/> />
); );
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
@ -224,6 +226,7 @@ export const DetailedStatus: React.FC<{
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
visible={showMedia} visible={showMedia}
onToggleVisibility={onToggleMediaVisibility} onToggleVisibility={onToggleMediaVisibility}
matchedFilters={status.get('matched_media_filters')}
/> />
); );
} }

View File

@ -136,6 +136,7 @@ class Video extends PureComponent {
muted: PropTypes.bool, muted: PropTypes.bool,
componentIndex: PropTypes.number, componentIndex: PropTypes.number,
autoFocus: PropTypes.bool, autoFocus: PropTypes.bool,
matchedFilters: PropTypes.arrayOf(PropTypes.string),
}; };
static defaultProps = { static defaultProps = {
@ -535,7 +536,7 @@ class Video extends PureComponent {
} }
render () { render () {
const { preview, src, aspectRatio, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props; const { preview, src, aspectRatio, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus, matchedFilters } = this.props;
const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, revealed } = this.state; const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, revealed } = this.state;
const progress = Math.min((currentTime / duration) * 100, 100); const progress = Math.min((currentTime / duration) * 100, 100);
const muted = this.state.muted || volume === 0; const muted = this.state.muted || volume === 0;
@ -592,7 +593,7 @@ class Video extends PureComponent {
style={{ width: '100%' }} style={{ width: '100%' }}
/>} />}
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} /> <SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} matchedFilters={matchedFilters} />
<div className={classNames('video-player__controls', { active: paused || hovered })}> <div className={classNames('video-player__controls', { active: paused || hovered })}>
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>

View File

@ -30,12 +30,19 @@ export const makeGetStatus = () => {
} }
let filtered = false; let filtered = false;
let mediaFiltered = false;
if ((accountReblog || accountBase).get('id') !== me && filters) { if ((accountReblog || accountBase).get('id') !== me && filters) {
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList(); let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) { if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
return null; return null;
} }
filterResults = filterResults.filter(result => filters.has(result.get('filter')));
let mediaFilters = filterResults.filter(result => filters.getIn([result.get('filter'), 'filter_action']) === 'blur');
if (!mediaFilters.isEmpty()) {
mediaFiltered = mediaFilters.map(result => filters.getIn([result.get('filter'), 'title']));
}
filterResults = filterResults.filter(result => filters.has(result.get('filter')) && filters.getIn([result.get('filter'), 'filter_action']) !== 'blur');
if (!filterResults.isEmpty()) { if (!filterResults.isEmpty()) {
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title'])); filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
} }
@ -45,6 +52,7 @@ export const makeGetStatus = () => {
map.set('reblog', statusReblog); map.set('reblog', statusReblog);
map.set('account', accountBase); map.set('account', accountBase);
map.set('matched_filters', filtered); map.set('matched_filters', filtered);
map.set('matched_media_filters', mediaFiltered);
}); });
}, },
); );

View File

@ -33,7 +33,7 @@ class CustomFilter < ApplicationRecord
include Expireable include Expireable
include Redisable include Redisable
enum :action, { warn: 0, hide: 1 }, suffix: :action enum :action, { warn: 0, hide: 1, blur: 2 }, suffix: :action
belongs_to :account belongs_to :account
has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy

View File

@ -26,7 +26,7 @@
.fields-group .fields-group
= f.input :filter_action, = f.input :filter_action,
as: :radio_buttons, as: :radio_buttons,
collection: %i(warn hide), collection: %i(warn blur hide),
hint: t('simple_form.hints.filters.action'), hint: t('simple_form.hints.filters.action'),
include_blank: false, include_blank: false,
label_method: ->(action) { filter_action_label(action) }, label_method: ->(action) { filter_action_label(action) },

View File

@ -75,6 +75,7 @@ en:
filters: filters:
action: Chose which action to perform when a post matches the filter action: Chose which action to perform when a post matches the filter
actions: actions:
blur: Hide media behind a warning, without hiding the text itself
hide: Completely hide the filtered content, behaving as if it did not exist hide: Completely hide the filtered content, behaving as if it did not exist
warn: Hide the filtered content behind a warning mentioning the filter's title warn: Hide the filtered content behind a warning mentioning the filter's title
form_admin_settings: form_admin_settings:
@ -260,6 +261,7 @@ en:
name: Hashtag name: Hashtag
filters: filters:
actions: actions:
blur: Hide media with a warning
hide: Hide completely hide: Hide completely
warn: Hide with a warning warn: Hide with a warning
form_admin_settings: form_admin_settings:

View File

@ -45,7 +45,7 @@ module Mastodon
def api_versions def api_versions
{ {
mastodon: 4, mastodon: 5,
} }
end end