Add new filter action to blur media (#34256)
This commit is contained in:
		@@ -226,6 +226,7 @@ class MediaGallery extends PureComponent {
 | 
			
		||||
    visible: PropTypes.bool,
 | 
			
		||||
    autoplay: PropTypes.bool,
 | 
			
		||||
    onToggleVisibility: PropTypes.func,
 | 
			
		||||
    matchedFilters: PropTypes.arrayOf(PropTypes.string),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
@@ -296,7 +297,7 @@ class MediaGallery extends PureComponent {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { media, lang, sensitive, defaultWidth, autoplay } = this.props;
 | 
			
		||||
    const { media, lang, sensitive, defaultWidth, autoplay, matchedFilters } = this.props;
 | 
			
		||||
    const { visible } = this.state;
 | 
			
		||||
    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}>
 | 
			
		||||
        {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) && (
 | 
			
		||||
          <div className='media-gallery__actions'>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ interface Props {
 | 
			
		||||
  hidden?: boolean;
 | 
			
		||||
  sensitive: boolean;
 | 
			
		||||
  uncached?: boolean;
 | 
			
		||||
  matchedFilters?: string[];
 | 
			
		||||
  onClick: React.MouseEventHandler<HTMLButtonElement>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -13,6 +14,7 @@ export const SpoilerButton: React.FC<Props> = ({
 | 
			
		||||
  hidden = false,
 | 
			
		||||
  sensitive,
 | 
			
		||||
  uncached = false,
 | 
			
		||||
  matchedFilters,
 | 
			
		||||
  onClick,
 | 
			
		||||
}) => {
 | 
			
		||||
  let warning;
 | 
			
		||||
@@ -28,6 +30,20 @@ export const SpoilerButton: React.FC<Props> = ({
 | 
			
		||||
    action = (
 | 
			
		||||
      <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) {
 | 
			
		||||
    warning = (
 | 
			
		||||
      <FormattedMessage
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,7 @@ export const defaultMediaVisibility = (status) => {
 | 
			
		||||
    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({
 | 
			
		||||
@@ -470,6 +470,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
                defaultWidth={this.props.cachedMediaWidth}
 | 
			
		||||
                visible={this.state.showMedia}
 | 
			
		||||
                onToggleVisibility={this.handleToggleMediaVisibility}
 | 
			
		||||
                matchedFilters={status.get('matched_media_filters')}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
@@ -498,6 +499,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
                blurhash={attachment.get('blurhash')}
 | 
			
		||||
                visible={this.state.showMedia}
 | 
			
		||||
                onToggleVisibility={this.handleToggleMediaVisibility}
 | 
			
		||||
                matchedFilters={status.get('matched_media_filters')}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
@@ -522,6 +524,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
                deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
 | 
			
		||||
                visible={this.state.showMedia}
 | 
			
		||||
                onToggleVisibility={this.handleToggleMediaVisibility}
 | 
			
		||||
                matchedFilters={status.get('matched_media_filters')}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,7 @@ class Audio extends PureComponent {
 | 
			
		||||
    volume: PropTypes.number,
 | 
			
		||||
    muted: PropTypes.bool,
 | 
			
		||||
    deployPictureInPicture: PropTypes.func,
 | 
			
		||||
    matchedFilters: PropTypes.arrayOf(PropTypes.string),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
@@ -472,7 +473,7 @@ class Audio extends PureComponent {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  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 progress = Math.min((currentTime / duration) * 100, 100);
 | 
			
		||||
    const muted = this.state.muted || volume === 0;
 | 
			
		||||
@@ -514,7 +515,7 @@ class Audio extends PureComponent {
 | 
			
		||||
          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
 | 
			
		||||
          src={this.props.poster}
 | 
			
		||||
 
 | 
			
		||||
@@ -175,6 +175,7 @@ export const DetailedStatus: React.FC<{
 | 
			
		||||
          onOpenMedia={onOpenMedia}
 | 
			
		||||
          visible={showMedia}
 | 
			
		||||
          onToggleVisibility={onToggleMediaVisibility}
 | 
			
		||||
          matchedFilters={status.get('matched_media_filters')}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
 | 
			
		||||
@@ -201,6 +202,7 @@ export const DetailedStatus: React.FC<{
 | 
			
		||||
          blurhash={attachment.get('blurhash')}
 | 
			
		||||
          height={150}
 | 
			
		||||
          onToggleVisibility={onToggleMediaVisibility}
 | 
			
		||||
          matchedFilters={status.get('matched_media_filters')}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
 | 
			
		||||
@@ -224,6 +226,7 @@ export const DetailedStatus: React.FC<{
 | 
			
		||||
          sensitive={status.get('sensitive')}
 | 
			
		||||
          visible={showMedia}
 | 
			
		||||
          onToggleVisibility={onToggleMediaVisibility}
 | 
			
		||||
          matchedFilters={status.get('matched_media_filters')}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -136,6 +136,7 @@ class Video extends PureComponent {
 | 
			
		||||
    muted: PropTypes.bool,
 | 
			
		||||
    componentIndex: PropTypes.number,
 | 
			
		||||
    autoFocus: PropTypes.bool,
 | 
			
		||||
    matchedFilters: PropTypes.arrayOf(PropTypes.string),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
@@ -535,7 +536,7 @@ class Video extends PureComponent {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 progress = Math.min((currentTime / duration) * 100, 100);
 | 
			
		||||
    const muted = this.state.muted || volume === 0;
 | 
			
		||||
@@ -592,7 +593,7 @@ class Video extends PureComponent {
 | 
			
		||||
            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='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
 | 
			
		||||
 
 | 
			
		||||
@@ -30,12 +30,19 @@ export const makeGetStatus = () => {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let filtered = false;
 | 
			
		||||
      let mediaFiltered = false;
 | 
			
		||||
      if ((accountReblog || accountBase).get('id') !== me && filters) {
 | 
			
		||||
        let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
 | 
			
		||||
        if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
 | 
			
		||||
          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()) {
 | 
			
		||||
          filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
 | 
			
		||||
        }
 | 
			
		||||
@@ -45,6 +52,7 @@ export const makeGetStatus = () => {
 | 
			
		||||
        map.set('reblog', statusReblog);
 | 
			
		||||
        map.set('account', accountBase);
 | 
			
		||||
        map.set('matched_filters', filtered);
 | 
			
		||||
        map.set('matched_media_filters', mediaFiltered);
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ class CustomFilter < ApplicationRecord
 | 
			
		||||
  include Expireable
 | 
			
		||||
  include Redisable
 | 
			
		||||
 | 
			
		||||
  enum :action, { warn: 0, hide: 1 }, suffix: :action
 | 
			
		||||
  enum :action, { warn: 0, hide: 1, blur: 2 }, suffix: :action
 | 
			
		||||
 | 
			
		||||
  belongs_to :account
 | 
			
		||||
  has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@
 | 
			
		||||
.fields-group
 | 
			
		||||
  = f.input :filter_action,
 | 
			
		||||
            as: :radio_buttons,
 | 
			
		||||
            collection: %i(warn hide),
 | 
			
		||||
            collection: %i(warn blur hide),
 | 
			
		||||
            hint: t('simple_form.hints.filters.action'),
 | 
			
		||||
            include_blank: false,
 | 
			
		||||
            label_method: ->(action) { filter_action_label(action) },
 | 
			
		||||
 
 | 
			
		||||
@@ -75,6 +75,7 @@ en:
 | 
			
		||||
      filters:
 | 
			
		||||
        action: Chose which action to perform when a post matches the filter
 | 
			
		||||
        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
 | 
			
		||||
          warn: Hide the filtered content behind a warning mentioning the filter's title
 | 
			
		||||
      form_admin_settings:
 | 
			
		||||
@@ -260,6 +261,7 @@ en:
 | 
			
		||||
        name: Hashtag
 | 
			
		||||
      filters:
 | 
			
		||||
        actions:
 | 
			
		||||
          blur: Hide media with a warning
 | 
			
		||||
          hide: Hide completely
 | 
			
		||||
          warn: Hide with a warning
 | 
			
		||||
      form_admin_settings:
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ module Mastodon
 | 
			
		||||
 | 
			
		||||
    def api_versions
 | 
			
		||||
      {
 | 
			
		||||
        mastodon: 4,
 | 
			
		||||
        mastodon: 5,
 | 
			
		||||
      }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user