~cytrogen/masto-fe

80a5c16ae1b2371e4896c5fc25abe2feb882bbc8 — Claire 2 years ago b052a7e + c7c6f02
Merge branch 'main' into glitch-soc/merge-upstream
M .devcontainer/Dockerfile => .devcontainer/Dockerfile +1 -1
@@ 1,5 1,5 @@
# For details, see https://github.com/devcontainers/images/tree/main/src/ruby
FROM mcr.microsoft.com/devcontainers/ruby:0-3.2-bullseye
FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye

# Install Rails
# RUN gem install rails webdrivers

M Gemfile.lock => Gemfile.lock +54 -54
@@ 18,40 18,40 @@ GIT
GEM
  remote: https://rubygems.org/
  specs:
    actioncable (6.1.7.3)
      actionpack (= 6.1.7.3)
      activesupport (= 6.1.7.3)
    actioncable (6.1.7.4)
      actionpack (= 6.1.7.4)
      activesupport (= 6.1.7.4)
      nio4r (~> 2.0)
      websocket-driver (>= 0.6.1)
    actionmailbox (6.1.7.3)
      actionpack (= 6.1.7.3)
      activejob (= 6.1.7.3)
      activerecord (= 6.1.7.3)
      activestorage (= 6.1.7.3)
      activesupport (= 6.1.7.3)
    actionmailbox (6.1.7.4)
      actionpack (= 6.1.7.4)
      activejob (= 6.1.7.4)
      activerecord (= 6.1.7.4)
      activestorage (= 6.1.7.4)
      activesupport (= 6.1.7.4)
      mail (>= 2.7.1)
    actionmailer (6.1.7.3)
      actionpack (= 6.1.7.3)
      actionview (= 6.1.7.3)
      activejob (= 6.1.7.3)
      activesupport (= 6.1.7.3)
    actionmailer (6.1.7.4)
      actionpack (= 6.1.7.4)
      actionview (= 6.1.7.4)
      activejob (= 6.1.7.4)
      activesupport (= 6.1.7.4)
      mail (~> 2.5, >= 2.5.4)
      rails-dom-testing (~> 2.0)
    actionpack (6.1.7.3)
      actionview (= 6.1.7.3)
      activesupport (= 6.1.7.3)
    actionpack (6.1.7.4)
      actionview (= 6.1.7.4)
      activesupport (= 6.1.7.4)
      rack (~> 2.0, >= 2.0.9)
      rack-test (>= 0.6.3)
      rails-dom-testing (~> 2.0)
      rails-html-sanitizer (~> 1.0, >= 1.2.0)
    actiontext (6.1.7.3)
      actionpack (= 6.1.7.3)
      activerecord (= 6.1.7.3)
      activestorage (= 6.1.7.3)
      activesupport (= 6.1.7.3)
    actiontext (6.1.7.4)
      actionpack (= 6.1.7.4)
      activerecord (= 6.1.7.4)
      activestorage (= 6.1.7.4)
      activesupport (= 6.1.7.4)
      nokogiri (>= 1.8.5)
    actionview (6.1.7.3)
      activesupport (= 6.1.7.3)
    actionview (6.1.7.4)
      activesupport (= 6.1.7.4)
      builder (~> 3.1)
      erubi (~> 1.4)
      rails-dom-testing (~> 2.0)


@@ 61,22 61,22 @@ GEM
      activemodel (>= 4.1, < 7.1)
      case_transform (>= 0.2)
      jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
    activejob (6.1.7.3)
      activesupport (= 6.1.7.3)
    activejob (6.1.7.4)
      activesupport (= 6.1.7.4)
      globalid (>= 0.3.6)
    activemodel (6.1.7.3)
      activesupport (= 6.1.7.3)
    activerecord (6.1.7.3)
      activemodel (= 6.1.7.3)
      activesupport (= 6.1.7.3)
    activestorage (6.1.7.3)
      actionpack (= 6.1.7.3)
      activejob (= 6.1.7.3)
      activerecord (= 6.1.7.3)
      activesupport (= 6.1.7.3)
    activemodel (6.1.7.4)
      activesupport (= 6.1.7.4)
    activerecord (6.1.7.4)
      activemodel (= 6.1.7.4)
      activesupport (= 6.1.7.4)
    activestorage (6.1.7.4)
      actionpack (= 6.1.7.4)
      activejob (= 6.1.7.4)
      activerecord (= 6.1.7.4)
      activesupport (= 6.1.7.4)
      marcel (~> 1.0)
      mini_mime (>= 1.1.0)
    activesupport (6.1.7.3)
    activesupport (6.1.7.4)
      concurrent-ruby (~> 1.0, >= 1.0.2)
      i18n (>= 1.6, < 2)
      minitest (>= 5.1)


@@ 412,7 412,7 @@ GEM
    mime-types-data (3.2023.0218.1)
    mini_mime (1.1.2)
    mini_portile2 (2.8.2)
    minitest (5.18.0)
    minitest (5.18.1)
    msgpack (1.7.1)
    multi_json (1.15.0)
    multipart-post (2.3.0)


@@ 511,20 511,20 @@ GEM
      rack
    rack-test (2.1.0)
      rack (>= 1.3)
    rails (6.1.7.3)
      actioncable (= 6.1.7.3)
      actionmailbox (= 6.1.7.3)
      actionmailer (= 6.1.7.3)
      actionpack (= 6.1.7.3)
      actiontext (= 6.1.7.3)
      actionview (= 6.1.7.3)
      activejob (= 6.1.7.3)
      activemodel (= 6.1.7.3)
      activerecord (= 6.1.7.3)
      activestorage (= 6.1.7.3)
      activesupport (= 6.1.7.3)
    rails (6.1.7.4)
      actioncable (= 6.1.7.4)
      actionmailbox (= 6.1.7.4)
      actionmailer (= 6.1.7.4)
      actionpack (= 6.1.7.4)
      actiontext (= 6.1.7.4)
      actionview (= 6.1.7.4)
      activejob (= 6.1.7.4)
      activemodel (= 6.1.7.4)
      activerecord (= 6.1.7.4)
      activestorage (= 6.1.7.4)
      activesupport (= 6.1.7.4)
      bundler (>= 1.15.0)
      railties (= 6.1.7.3)
      railties (= 6.1.7.4)
      sprockets-rails (>= 2.0.0)
    rails-controller-testing (1.0.5)
      actionpack (>= 5.0.1.rc1)


@@ 539,9 539,9 @@ GEM
    rails-i18n (6.0.0)
      i18n (>= 0.7, < 2)
      railties (>= 6.0.0, < 7)
    railties (6.1.7.3)
      actionpack (= 6.1.7.3)
      activesupport (= 6.1.7.3)
    railties (6.1.7.4)
      actionpack (= 6.1.7.4)
      activesupport (= 6.1.7.4)
      method_source
      rake (>= 12.2)
      thor (~> 1.0)

M app/javascript/mastodon/actions/compose.js => app/javascript/mastodon/actions/compose.js +2 -2
@@ 129,13 129,13 @@ export function resetCompose() {
  };
}

export const focusCompose = (routerHistory, defaultText) => dispatch => {
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
  dispatch({
    type: COMPOSE_FOCUS,
    defaultText,
  });

  ensureComposeIsVisible(routerHistory);
  ensureComposeIsVisible(getState, routerHistory);
};

export function mentionCompose(account, routerHistory) {

M app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx => app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx +6 -4
@@ 16,9 16,11 @@ export const ExplorePrompt = () => (
    <h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
    <p><FormattedMessage id='home.explore_prompt.body' defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" /></p>

    <div className='dismissable-banner__message__actions'>
      <Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
      <Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
    <div className='dismissable-banner__message__actions__wrapper'>
      <div className='dismissable-banner__message__actions'>
        <Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
        <Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
      </div>
    </div>
  </DismissableBanner>
);
\ No newline at end of file
);

M app/javascript/mastodon/features/home_timeline/index.jsx => app/javascript/mastodon/features/home_timeline/index.jsx +12 -5
@@ 33,9 33,11 @@ const messages = defineMessages({

const getHomeFeedSpeed = createSelector([
  state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
  state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
  state => state.get('statuses'),
], (statusIds, statusMap) => {
  const statuses = statusIds.map(id => statusMap.get(id)).filter(status => status.get('account') !== me).take(20);
], (statusIds, pendingStatusIds, statusMap) => {
  const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
  const statuses = recentStatusIds.map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
  const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
  const newest = new Date(statuses.getIn([0, 'created_at'], 0));
  const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds


@@ 46,9 48,14 @@ const getHomeFeedSpeed = createSelector([
  };
});

const homeTooSlow = createSelector(getHomeFeedSpeed, speed =>
  speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
  || (Date.now() - speed.newest) > (1000 * 3600) // If the most recent post is from over an hour ago
const homeTooSlow = createSelector([
  state => state.getIn(['timelines', 'home', 'isLoading']),
  state => state.getIn(['timelines', 'home', 'isPartial']),
  getHomeFeedSpeed,
], (isLoading, isPartial, speed) =>
  !isLoading && !isPartial // Only if the home feed has finished loading
  && (speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
  || (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
);

const mapStateToProps = state => ({

M app/javascript/mastodon/features/ui/components/header.jsx => app/javascript/mastodon/features/ui/components/header.jsx +13 -5
@@ 1,7 1,7 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';

import { FormattedMessage } from 'react-intl';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';

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



@@ 10,6 10,7 @@ import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import { fetchServer } from 'mastodon/actions/server';
import { Avatar } from 'mastodon/components/avatar';
import { Icon } from 'mastodon/components/icon';
import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
import { registrationsOpen, me } from 'mastodon/initial_state';



@@ 21,6 22,10 @@ const Account = connect(state => ({
  </Link>
));

const messages = defineMessages({
  search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
});

const mapStateToProps = (state) => ({
  signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
});


@@ 44,7 49,8 @@ class Header extends PureComponent {
    openClosedRegistrationsModal: PropTypes.func,
    location: PropTypes.object,
    signupUrl: PropTypes.string.isRequired,
    dispatchServer: PropTypes.func
    dispatchServer: PropTypes.func,
    intl: PropTypes.object.isRequired,
  };

  componentDidMount () {


@@ 54,14 60,15 @@ class Header extends PureComponent {

  render () {
    const { signedIn } = this.context.identity;
    const { location, openClosedRegistrationsModal, signupUrl } = this.props;
    const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props;

    let content;

    if (signedIn) {
      content = (
        <>
          {location.pathname !== '/publish' && <Link to='/publish' className='button'><FormattedMessage id='compose_form.publish_form' defaultMessage='Publish' /></Link>}
          {location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' /></Link>}
          {location.pathname !== '/publish' && <Link to='/publish' className='button button-secondary'><FormattedMessage id='compose_form.publish_form' defaultMessage='New post' /></Link>}
          <Account />
        </>
      );


@@ 84,6 91,7 @@ class Header extends PureComponent {

      content = (
        <>
          {location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' /></Link>}
          {signupButton}
          <a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
        </>


@@ 106,4 114,4 @@ class Header extends PureComponent {

}

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header));
export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)));

M app/javascript/mastodon/locales/en.json => app/javascript/mastodon/locales/en.json +1 -1
@@ 147,7 147,7 @@
  "compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
  "compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
  "compose_form.publish": "Publish",
  "compose_form.publish_form": "Publish",
  "compose_form.publish_form": "New post",
  "compose_form.publish_loud": "{publish}!",
  "compose_form.save_changes": "Save changes",
  "compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",

M app/javascript/styles/mastodon/components.scss => app/javascript/styles/mastodon/components.scss +36 -44
@@ 133,12 133,13 @@
    color: $darker-text-color;
    background: transparent;
    padding: 6px 17px;
    border: 1px solid $ui-primary-color;
    border: 1px solid lighten($ui-base-color, 12%);

    &:active,
    &:focus,
    &:hover {
      border-color: lighten($ui-primary-color, 4%);
      background: lighten($ui-base-color, 4%);
      border-color: lighten($ui-base-color, 16%);
      color: lighten($darker-text-color, 4%);
      text-decoration: none;
    }


@@ 3146,7 3147,7 @@ $ui-header-height: 55px;
.column-back-button {
  box-sizing: border-box;
  width: 100%;
  background: lighten($ui-base-color, 4%);
  background: $ui-base-color;
  border-radius: 4px 4px 0 0;
  color: $highlight-text-color;
  cursor: pointer;


@@ 3154,6 3155,7 @@ $ui-header-height: 55px;
  font-size: 16px;
  line-height: inherit;
  border: 0;
  border-bottom: 1px solid lighten($ui-base-color, 8%);
  text-align: unset;
  padding: 15px;
  margin: 0;


@@ 3166,7 3168,7 @@ $ui-header-height: 55px;
}

.column-header__back-button {
  background: lighten($ui-base-color, 4%);
  background: $ui-base-color;
  border: 0;
  font-family: inherit;
  color: $highlight-text-color;


@@ 3201,7 3203,7 @@ $ui-header-height: 55px;
  padding: 15px;
  position: absolute;
  inset-inline-end: 0;
  top: -48px;
  top: -50px;
}

.react-toggle {


@@ 3882,7 3884,8 @@ a.status-card.compact:hover {
.column-header {
  display: flex;
  font-size: 16px;
  background: lighten($ui-base-color, 4%);
  background: $ui-base-color;
  border-bottom: 1px solid lighten($ui-base-color, 8%);
  border-radius: 4px 4px 0 0;
  flex: 0 0 auto;
  cursor: pointer;


@@ 3937,7 3940,7 @@ a.status-card.compact:hover {
}

.column-header__button {
  background: lighten($ui-base-color, 4%);
  background: $ui-base-color;
  border: 0;
  color: $darker-text-color;
  cursor: pointer;


@@ 3945,16 3948,15 @@ a.status-card.compact:hover {
  padding: 0 15px;

  &:hover {
    color: lighten($darker-text-color, 7%);
    color: lighten($darker-text-color, 4%);
  }

  &.active {
    color: $primary-text-color;
    background: lighten($ui-base-color, 8%);
    background: lighten($ui-base-color, 4%);

    &:hover {
      color: $primary-text-color;
      background: lighten($ui-base-color, 8%);
    }
  }



@@ 3968,6 3970,7 @@ a.status-card.compact:hover {
  max-height: 70vh;
  overflow: hidden;
  overflow-y: auto;
  border-bottom: 1px solid lighten($ui-base-color, 8%);
  color: $darker-text-color;
  transition: max-height 150ms ease-in-out, opacity 300ms linear;
  opacity: 1;


@@ 3987,13 3990,13 @@ a.status-card.compact:hover {
    height: 0;
    background: transparent;
    border: 0;
    border-top: 1px solid lighten($ui-base-color, 12%);
    border-top: 1px solid lighten($ui-base-color, 8%);
    margin: 10px 0;
  }
}

.column-header__collapsible-inner {
  background: lighten($ui-base-color, 8%);
  background: $ui-base-color;
  padding: 15px;
}



@@ 4406,17 4409,13 @@ a.status-card.compact:hover {
  color: $primary-text-color;
  margin-bottom: 4px;
  display: block;
  background-color: $base-overlay-background;
  text-transform: uppercase;
  background-color: rgba($black, 0.45);
  backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
  font-size: 11px;
  font-weight: 500;
  padding: 4px;
  text-transform: uppercase;
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 4px;
  opacity: 0.7;

  &:hover {
    opacity: 1;
  }
}

.setting-toggle {


@@ 4476,6 4475,7 @@ a.status-card.compact:hover {

.follow_requests-unlocked_explanation {
  background: darken($ui-base-color, 4%);
  border-bottom: 1px solid lighten($ui-base-color, 8%);
  contain: initial;
  flex-grow: 0;
}


@@ 6160,6 6160,7 @@ a.status-card.compact:hover {
  display: block;
  color: $white;
  background: rgba($black, 0.65);
  backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 11px;


@@ 6837,24 6838,6 @@ a.status-card.compact:hover {
      }
    }
  }

  &.directory__section-headline {
    background: darken($ui-base-color, 2%);
    border-bottom-color: transparent;

    a,
    button {
      &.active {
        &::before {
          display: none;
        }

        &::after {
          border-color: transparent transparent darken($ui-base-color, 7%);
        }
      }
    }
  }
}

.filter-form {


@@ 7369,7 7352,6 @@ noscript {

.account__header {
  overflow: hidden;
  background: lighten($ui-base-color, 4%);

  &.inactive {
    opacity: 0.5;


@@ 7391,6 7373,7 @@ noscript {
    height: 145px;
    position: relative;
    background: darken($ui-base-color, 4%);
    border-bottom: 1px solid lighten($ui-base-color, 8%);

    img {
      object-fit: cover;


@@ 7404,7 7387,7 @@ noscript {
  &__bar {
    position: relative;
    padding: 0 20px;
    border-bottom: 1px solid lighten($ui-base-color, 12%);
    border-bottom: 1px solid lighten($ui-base-color, 8%);

    .avatar {
      display: block;


@@ 7413,7 7396,7 @@ noscript {

      .account__avatar {
        background: darken($ui-base-color, 8%);
        border: 2px solid lighten($ui-base-color, 4%);
        border: 2px solid $ui-base-color;
      }
    }
  }


@@ 8785,9 8768,18 @@ noscript {

    &__actions {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 4px;
      margin-top: 30px;

      &__wrapper {
        display: flex;
        margin-top: 30px;
      }

      .button {
        display: block;
        flex-grow: 1;
      }
    }

    .button-tertiary {

A app/lib/attachment_batch.rb => app/lib/attachment_batch.rb +111 -0
@@ 0,0 1,111 @@
# frozen_string_literal: true

class AttachmentBatch
  # Maximum amount of objects you can delete in an S3 API call. It's
  # important to remember that this does not correspond to the number
  # of records in the batch, since records can have multiple attachments
  LIMIT = 1_000

  # Attributes generated and maintained by Paperclip (not all of them
  # are always used on every class, however)
  NULLABLE_ATTRIBUTES = %w(
    file_name
    content_type
    file_size
    fingerprint
    created_at
    updated_at
  ).freeze

  # Styles that are always present even when not explicitly defined
  BASE_STYLES = %i(original).freeze

  attr_reader :klass, :records, :storage_mode

  def initialize(klass, records)
    @klass            = klass
    @records          = records
    @storage_mode     = Paperclip::Attachment.default_options[:storage]
    @attachment_names = klass.attachment_definitions.keys
  end

  def delete
    remove_files
    batch.delete_all
  end

  def clear
    remove_files
    batch.update_all(nullified_attributes) # rubocop:disable Rails/SkipsModelValidations
  end

  private

  def batch
    klass.where(id: records.map(&:id))
  end

  def remove_files
    keys = []

    logger.debug { "Preparing to delete attachments for #{records.size} records" }

    records.each do |record|
      @attachment_names.each do |attachment_name|
        attachment = record.public_send(attachment_name)
        styles     = BASE_STYLES | attachment.styles.keys

        next if attachment.blank?

        styles.each do |style|
          case @storage_mode
          when :s3
            logger.debug { "Adding #{attachment.path(style)} to batch for deletion" }
            keys << attachment.style_name_as_path(style)
          when :filesystem
            logger.debug { "Deleting #{attachment.path(style)}" }
            path = attachment.path(style)
            FileUtils.remove_file(path, true)

            begin
              FileUtils.rmdir(File.dirname(path), parents: true)
            rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR, Errno::EACCES
              # Ignore failure to delete a directory, with the same ignored errors
              # as Paperclip
            end
          when :fog
            logger.debug { "Deleting #{attachment.path(style)}" }
            attachment.directory.files.new(key: attachment.path(style)).destroy
          end
        end
      end
    end

    return unless storage_mode == :s3

    # We can batch deletes over S3, but there is a limit of how many
    # objects can be processed at once, so we have to potentially
    # separate them into multiple calls.

    keys.each_slice(LIMIT) do |keys_slice|
      logger.debug { "Deleting #{keys_slice.size} objects" }

      bucket.delete_objects(delete: {
        objects: keys_slice.map { |key| { key: key } },
        quiet: true,
      })
    end
  end

  def bucket
    @bucket ||= records.first.public_send(@attachment_names.first).s3_bucket
  end

  def nullified_attributes
    @attachment_names.flat_map { |attachment_name| NULLABLE_ATTRIBUTES.map { |attribute| "#{attachment_name}_#{attribute}" } & klass.column_names }.index_with(nil)
  end

  def logger
    Rails.logger
  end
end

M app/lib/vacuum/media_attachments_vacuum.rb => app/lib/vacuum/media_attachments_vacuum.rb +5 -5
@@ 15,15 15,15 @@ class Vacuum::MediaAttachmentsVacuum
  private

  def vacuum_cached_files!
    media_attachments_past_retention_period.find_each do |media_attachment|
      media_attachment.file.destroy
      media_attachment.thumbnail.destroy
      media_attachment.save
    media_attachments_past_retention_period.find_in_batches do |media_attachments|
      AttachmentBatch.new(MediaAttachment, media_attachments).clear
    end
  end

  def vacuum_orphaned_records!
    orphaned_media_attachments.in_batches.destroy_all
    orphaned_media_attachments.find_in_batches do |media_attachments|
      AttachmentBatch.new(MediaAttachment, media_attachments).delete
    end
  end

  def media_attachments_past_retention_period

M app/services/clear_domain_media_service.rb => app/services/clear_domain_media_service.rb +6 -24
@@ 10,14 10,6 @@ class ClearDomainMediaService < BaseService

  private

  def invalidate_association_caches!(status_ids)
    # Normally, associated models of a status are immutable (except for accounts)
    # so they are aggressively cached. After updating the media attachments to no
    # longer point to a local file, we need to clear the cache to make those
    # changes appear in the API and UI
    Rails.cache.delete_multi(status_ids.map { |id| "statuses/#{id}" })
  end

  def clear_media!
    clear_account_images!
    clear_account_attachments!


@@ 25,31 17,21 @@ class ClearDomainMediaService < BaseService
  end

  def clear_account_images!
    blocked_domain_accounts.reorder(nil).find_each do |account|
      account.avatar.destroy if account.avatar&.exists?
      account.header.destroy if account.header&.exists?
      account.save
    blocked_domain_accounts.reorder(nil).find_in_batches do |accounts|
      AttachmentBatch.new(Account, accounts).clear
    end
  end

  def clear_account_attachments!
    media_from_blocked_domain.reorder(nil).find_in_batches do |attachments|
      affected_status_ids = []

      attachments.each do |attachment|
        affected_status_ids << attachment.status_id if attachment.status_id.present?

        attachment.file.destroy if attachment.file&.exists?
        attachment.type = :unknown
        attachment.save
      end

      invalidate_association_caches!(affected_status_ids) unless affected_status_ids.empty?
      AttachmentBatch.new(MediaAttachment, attachments).clear
    end
  end

  def clear_emojos!
    emojis_from_blocked_domains.destroy_all
    emojis_from_blocked_domains.find_in_batches do |custom_emojis|
      AttachmentBatch.new(CustomEmoji, custom_emojis).delete
    end
  end

  def blocked_domain

M app/views/admin/domain_blocks/confirm_suspension.html.haml => app/views/admin/domain_blocks/confirm_suspension.html.haml +1 -1
@@ 1,7 1,7 @@
- content_for :page_title do
  = t('.title', domain: Addressable::IDNA.to_unicode(@domain_block.domain))

= simple_form_for @domain_block, url: admin_domain_blocks_path(@domain_block) do |f|
= simple_form_for @domain_block, url: admin_domain_blocks_path, method: :post do |f|

  %p.hint= t('.preamble_html', domain: Addressable::IDNA.to_unicode(@domain_block.domain))
  %ul.hint

M spec/features/admin/domain_blocks_spec.rb => spec/features/admin/domain_blocks_spec.rb +2 -2
@@ 53,7 53,7 @@ describe 'blocking domains through the moderation interface' do
      # Confirming updates the block
      click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm')

      expect(domain_block.reload.severity).to eq 'silence'
      expect(domain_block.reload.severity).to eq 'suspend'
    end
  end



@@ 72,7 72,7 @@ describe 'blocking domains through the moderation interface' do
      # Confirming updates the block
      click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm')

      expect(domain_block.reload.severity).to eq 'silence'
      expect(domain_block.reload.severity).to eq 'suspend'
    end
  end
end

M yarn.lock => yarn.lock +37 -33
@@ 5841,9 5841,9 @@ glob-parent@^6.0.2:
    is-glob "^4.0.3"

glob@^10.2.5, glob@^10.2.6:
  version "10.2.7"
  resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.7.tgz#9dd2828cd5bc7bd861e7738d91e7113dda41d7d8"
  integrity sha512-jTKehsravOJo8IJxUGfZILnkvVJM/MOfHRs8QcXolVef2zNI9Tqyy5+SeuOAZd3upViEZQLyFpQhYiHLrMUNmA==
  version "10.3.0"
  resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.0.tgz#763d02a894f3cdfc521b10bbbbc8e0309e750cce"
  integrity sha512-AQ1/SB9HH0yCx1jXAT4vmCbTOPe5RQ+kCurjbel5xSCGhebumUv+GJZfa1rEqor3XIViqwSEmlkZCQD43RWrBg==
  dependencies:
    foreground-child "^3.1.0"
    jackspeak "^2.0.3"


@@ 8042,9 8042,9 @@ minimatch@^5.0.1:
    brace-expansion "^2.0.1"

minimatch@^9.0.1:
  version "9.0.1"
  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253"
  integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==
  version "9.0.2"
  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.2.tgz#397e387fff22f6795844d00badc903a3d5de7057"
  integrity sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==
  dependencies:
    brace-expansion "^2.0.1"



@@ 8769,15 8769,20 @@ performance-now@^2.1.0:
  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
  integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==

pg-cloudflare@^1.1.0:
  version "1.1.0"
  resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.0.tgz#833d70870d610d14bf9df7afb40e1cba310c17a0"
  integrity sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA==
pg-cloudflare@^1.1.1:
  version "1.1.1"
  resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98"
  integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==

pg-connection-string@^2.6.0:
  version "2.6.0"
  resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.0.tgz#12a36cc4627df19c25cc1b9b736cc39ee1f73ae8"
  integrity sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg==
  version "2.6.1"
  resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb"
  integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==

pg-connection-string@^2.6.1:
  version "2.6.1"
  resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb"
  integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==

pg-int8@1.0.1:
  version "1.0.1"


@@ 8789,10 8794,10 @@ pg-numeric@1.0.2:
  resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a"
  integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==

pg-pool@^3.6.0:
  version "3.6.0"
  resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.0.tgz#3190df3e4747a0d23e5e9e8045bcd99bda0a712e"
  integrity sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==
pg-pool@^3.6.1:
  version "3.6.1"
  resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.1.tgz#5a902eda79a8d7e3c928b77abf776b3cb7d351f7"
  integrity sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==

pg-protocol@*, pg-protocol@^1.6.0:
  version "1.6.0"


@@ 8824,19 8829,19 @@ pg-types@^4.0.1:
    postgres-range "^1.1.1"

pg@^8.5.0:
  version "8.11.0"
  resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.0.tgz#a37e534e94b57a7ed811e926f23a7c56385f55d9"
  integrity sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA==
  version "8.11.1"
  resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.1.tgz#297e0eb240306b1e9e4f55af8a3bae76ae4810b1"
  integrity sha512-utdq2obft07MxaDg0zBJI+l/M3mBRfIpEN3iSemsz0G5F2/VXx+XzqF4oxrbIZXQxt2AZzIUzyVg/YM6xOP/WQ==
  dependencies:
    buffer-writer "2.0.0"
    packet-reader "1.0.0"
    pg-connection-string "^2.6.0"
    pg-pool "^3.6.0"
    pg-connection-string "^2.6.1"
    pg-pool "^3.6.1"
    pg-protocol "^1.6.0"
    pg-types "^2.1.0"
    pgpass "1.x"
  optionalDependencies:
    pg-cloudflare "^1.1.0"
    pg-cloudflare "^1.1.1"

pgpass@1.x:
  version "1.0.5"


@@ 9582,9 9587,9 @@ react-redux-loading-bar@^5.0.4:
    react-lifecycles-compat "^3.0.4"

react-redux@^8.0.4:
  version "8.1.0"
  resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.0.tgz#4e147339f00bbaac7196bc42bc99e6fc412846e7"
  integrity sha512-CtHZzAOxi7GQvTph4dVLWwZHAWUjV2kMEQtk50OrN8z3gKxpWg3Tz7JfDw32N3Rpd7fh02z73cF6yZkK467gbQ==
  version "8.1.1"
  resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.1.tgz#8e740f3fd864a4cd0de5ba9cdc8ad39cc9e7c81a"
  integrity sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==
  dependencies:
    "@babel/runtime" "^7.12.1"
    "@types/hoist-non-react-statics" "^3.3.1"


@@ 9702,9 9707,9 @@ react-test-renderer@^18.2.0:
    scheduler "^0.23.0"

react-textarea-autosize@*, react-textarea-autosize@^8.4.1:
  version "8.4.1"
  resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.4.1.tgz#bcfc5462727014b808b14ee916c01e275e8a8335"
  integrity sha512-aD2C+qK6QypknC+lCMzteOdIjoMbNlgSFmJjCV+DrfTPwp59i/it9mMNf2HDzvRjQgKAyBDPyLJhcrzElf2U4Q==
  version "8.5.0"
  resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.0.tgz#bb0f7faf9849850f1c20b6e7fac0309d4b92f87b"
  integrity sha512-cp488su3U9RygmHmGpJp0KEt0i/+57KCK33XVPH+50swVRBhIZYh0fGduz2YLKXwl9vSKBZ9HUXcg9PQXUXqIw==
  dependencies:
    "@babel/runtime" "^7.20.13"
    use-composed-ref "^1.3.0"


@@ 10171,9 10176,9 @@ sass-loader@^10.2.0:
    semver "^7.3.2"

sass@^1.62.1:
  version "1.63.4"
  resolved "https://registry.yarnpkg.com/sass/-/sass-1.63.4.tgz#caf60643321044c61f6a0fe638a07abbd31cfb5d"
  integrity sha512-Sx/+weUmK+oiIlI+9sdD0wZHsqpbgQg8wSwSnGBjwb5GwqFhYNwwnI+UWZtLjKvKyFlKkatRK235qQ3mokyPoQ==
  version "1.63.6"
  resolved "https://registry.yarnpkg.com/sass/-/sass-1.63.6.tgz#481610e612902e0c31c46b46cf2dad66943283ea"
  integrity sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==
  dependencies:
    chokidar ">=3.0.0 <4.0.0"
    immutable "^4.0.0"


@@ 10846,7 10851,6 @@ stringz@^2.1.0:
    char-regex "^1.0.2"

"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
  name strip-ansi-cjs
  version "6.0.1"
  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==