~cytrogen/masto-fe

ed10ae2693e83ac7fbac7273b2cf2ca424aca0c4 — ThibG 6 years ago 38d2882 + 81bf43c
Merge pull request #1138 from ThibG/glitch-soc/merge-upstream

Merge upstream changes
92 files changed, 1603 insertions(+), 414 deletions(-)

M .env.production.sample
M CHANGELOG.md
M app/controllers/admin/accounts_controller.rb
M app/controllers/admin/domain_blocks_controller.rb
M app/controllers/admin/instances_controller.rb
A app/controllers/api/v1/admin/account_actions_controller.rb
A app/controllers/api/v1/admin/accounts_controller.rb
A app/controllers/api/v1/admin/reports_controller.rb
M app/controllers/media_controller.rb
M app/controllers/media_proxy_controller.rb
M app/controllers/settings/identity_proofs_controller.rb
M app/javascript/flavours/glitch/components/media_gallery.js
M app/javascript/flavours/glitch/components/status.js
M app/javascript/flavours/glitch/features/compose/containers/options_container.js
M app/javascript/flavours/glitch/features/status/components/detailed_status.js
M app/javascript/flavours/glitch/styles/components/composer.scss
M app/javascript/mastodon/components/media_gallery.js
M app/javascript/mastodon/components/status.js
M app/javascript/mastodon/features/compose/components/upload_button.js
M app/javascript/mastodon/features/compose/containers/upload_button_container.js
M app/javascript/mastodon/features/status/components/detailed_status.js
M app/javascript/mastodon/locales/ar.json
M app/javascript/mastodon/locales/ca.json
M app/javascript/mastodon/locales/de.json
M app/javascript/mastodon/locales/defaultMessages.json
M app/javascript/mastodon/locales/en.json
M app/javascript/mastodon/locales/fi.json
M app/javascript/mastodon/locales/it.json
M app/javascript/mastodon/locales/ja.json
M app/javascript/mastodon/locales/nl.json
M app/javascript/mastodon/locales/sk.json
M app/javascript/mastodon/locales/sl.json
M app/javascript/mastodon/locales/zh-CN.json
M app/javascript/styles/mastodon/components.scss
M app/lib/activitypub/activity/create.rb
M app/lib/activitypub/activity/flag.rb
M app/lib/ostatus/activity/creation.rb
M app/models/account.rb
M app/models/account_filter.rb
M app/models/concerns/attachmentable.rb
M app/models/concerns/user_roles.rb
M app/models/custom_emoji.rb
M app/models/domain_block.rb
M app/models/instance.rb
M app/models/media_attachment.rb
M app/models/report.rb
M app/models/report_filter.rb
M app/models/user.rb
M app/serializers/initial_state_serializer.rb
A app/serializers/rest/admin/account_serializer.rb
A app/serializers/rest/admin/report_serializer.rb
M app/serializers/rest/instance_serializer.rb
M app/services/activitypub/process_account_service.rb
M app/services/block_domain_service.rb
M app/services/post_status_service.rb
M app/services/resolve_account_service.rb
M app/services/unblock_domain_service.rb
M app/services/update_remote_profile_service.rb
M app/views/admin/instances/index.html.haml
M app/views/stream_entries/_detailed_status.html.haml
M app/views/stream_entries/_simple_status.html.haml
M config/application.rb
M config/initializers/doorkeeper.rb
M config/locales/activerecord.fi.yml
M config/locales/activerecord.it.yml
M config/locales/devise.it.yml
M config/locales/doorkeeper.ca.yml
M config/locales/doorkeeper.co.yml
M config/locales/doorkeeper.cs.yml
M config/locales/doorkeeper.de.yml
M config/locales/doorkeeper.el.yml
M config/locales/doorkeeper.en.yml
M config/locales/doorkeeper.gl.yml
M config/locales/doorkeeper.it.yml
M config/locales/doorkeeper.ja.yml
M config/locales/doorkeeper.pl.yml
M config/locales/doorkeeper.sl.yml
M config/locales/doorkeeper.zh-CN.yml
M config/locales/it.yml
M config/locales/pl.yml
M config/locales/simple_form.it.yml
M config/locales/simple_form.ja.yml
M config/locales/sk.yml
M config/routes.rb
M lib/mastodon/version.rb
D lib/paperclip/audio_transcoder.rb
A lib/paperclip/type_corrector.rb
A spec/controllers/api/v1/admin/account_actions_controller_spec.rb
A spec/controllers/api/v1/admin/accounts_controller_spec.rb
A spec/controllers/api/v1/admin/reports_controller_spec.rb
M spec/models/account_spec.rb
M spec/models/domain_block_spec.rb
M .env.production.sample => .env.production.sample +1 -4
@@ 169,15 169,12 @@ STREAMING_CLUSTER_NUM=1
# Maximum allowed display name characters
# MAX_DISPLAY_NAME_CHARS=30

# Maximum image and video upload sizes
# Maximum image and video/audio upload sizes
# Units are in bytes
# 1048576 bytes equals 1 megabyte
# MAX_IMAGE_SIZE=8388608
# MAX_VIDEO_SIZE=41943040

# Maximum length of audio uploads in seconds
# MAX_AUDIO_LENGTH=60

# LDAP authentication (optional)
# LDAP_ENABLED=true
# LDAP_HOST=localhost

M CHANGELOG.md => CHANGELOG.md +39 -0
@@ 3,6 3,45 @@ Changelog

All notable changes to this project will be documented in this file.

## [2.9.2] - 2019-06-22
### Added

- Add `short_description` and `approval_required` to `GET /api/v1/instance` ([Gargron](https://github.com/tootsuite/mastodon/pull/11146))

### Changed

- Change camera icon to paperclip icon in upload form ([koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/11149))

### Fixed

- Fix audio-only OGG and WebM files not being processed as such ([Gargron](https://github.com/tootsuite/mastodon/pull/11151))
- Fix audio not being downloaded from remote servers ([Gargron](https://github.com/tootsuite/mastodon/pull/11145))

## [2.9.1] - 2019-06-22
### Added

- Add moderation API ([Gargron](https://github.com/tootsuite/mastodon/pull/9387))
- Add audio uploads ([Gargron](https://github.com/tootsuite/mastodon/pull/11123), [Gargron](https://github.com/tootsuite/mastodon/pull/11141))

### Changed

- Change domain blocks to automatically support subdomains ([Gargron](https://github.com/tootsuite/mastodon/pull/11138))
- Change Nanobox configuration to bring it up to date ([danhunsaker](https://github.com/tootsuite/mastodon/pull/11083))

### Removed

- Remove expensive counters from federation page in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11139))

### Fixed

- Fix converted media being saved with original extension and mime type ([Gargron](https://github.com/tootsuite/mastodon/pull/11130))
- Fix layout of identity proofs settings ([acid-chicken](https://github.com/tootsuite/mastodon/pull/11126))
- Fix active scope only returning suspended users ([ThibG](https://github.com/tootsuite/mastodon/pull/11111))
- Fix sanitizer making block level elements unreadable ([Gargron](https://github.com/tootsuite/mastodon/pull/10836))
- Fix label for site theme not being translated in admin UI ([palindromordnilap](https://github.com/tootsuite/mastodon/pull/11121))
- Fix statuses not being filtered irreversibly in web UI under some circumstances ([ThibG](https://github.com/tootsuite/mastodon/pull/11113))
- Fix scrolling behaviour in compose form ([ThibG](https://github.com/tootsuite/mastodon/pull/11093))

## [2.9.0] - 2019-06-13
### Added


M app/controllers/admin/accounts_controller.rb => app/controllers/admin/accounts_controller.rb +1 -0
@@ 127,6 127,7 @@ module Admin
        :by_domain,
        :active,
        :pending,
        :disabled,
        :silenced,
        :suspended,
        :username,

M app/controllers/admin/domain_blocks_controller.rb => app/controllers/admin/domain_blocks_controller.rb +1 -1
@@ 13,7 13,7 @@ module Admin
      authorize :domain_block, :create?

      @domain_block = DomainBlock.new(resource_params)
      existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil
      existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil

      if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
        @domain_block.save

M app/controllers/admin/instances_controller.rb => app/controllers/admin/instances_controller.rb +1 -1
@@ 18,7 18,7 @@ module Admin
      @blocks_count    = Block.where(target_account: Account.where(domain: params[:id])).count
      @available       = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
      @media_storage   = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
      @domain_block    = DomainBlock.find_by(domain: params[:id])
      @domain_block    = DomainBlock.rule_for(params[:id])
    end

    private

A app/controllers/api/v1/admin/account_actions_controller.rb => app/controllers/api/v1/admin/account_actions_controller.rb +32 -0
@@ 0,0 1,32 @@
# frozen_string_literal: true

class Api::V1::Admin::AccountActionsController < Api::BaseController
  before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }
  before_action :require_staff!
  before_action :set_account

  def create
    account_action                 = Admin::AccountAction.new(resource_params)
    account_action.target_account  = @account
    account_action.current_account = current_account
    account_action.save!

    render_empty
  end

  private

  def set_account
    @account = Account.find(params[:account_id])
  end

  def resource_params
    params.permit(
      :type,
      :report_id,
      :warning_preset_id,
      :text,
      :send_email_notification
    )
  end
end

A app/controllers/api/v1/admin/accounts_controller.rb => app/controllers/api/v1/admin/accounts_controller.rb +128 -0
@@ 0,0 1,128 @@
# frozen_string_literal: true

class Api::V1::Admin::AccountsController < Api::BaseController
  include Authorization
  include AccountableConcern

  LIMIT = 100

  before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
  before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
  before_action :require_staff!
  before_action :set_accounts, only: :index
  before_action :set_account, except: :index
  before_action :require_local_account!, only: [:enable, :approve, :reject]

  after_action :insert_pagination_headers, only: :index

  FILTER_PARAMS = %i(
    local
    remote
    by_domain
    active
    pending
    disabled
    silenced
    suspended
    username
    display_name
    email
    ip
    staff
  ).freeze

  PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze

  def index
    authorize :account, :index?
    render json: @accounts, each_serializer: REST::Admin::AccountSerializer
  end

  def show
    authorize @account, :show?
    render json: @account, serializer: REST::Admin::AccountSerializer
  end

  def enable
    authorize @account.user, :enable?
    @account.user.enable!
    log_action :enable, @account.user
    render json: @account, serializer: REST::Admin::AccountSerializer
  end

  def approve
    authorize @account.user, :approve?
    @account.user.approve!
    render json: @account, serializer: REST::Admin::AccountSerializer
  end

  def reject
    authorize @account.user, :reject?
    SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
    render json: @account, serializer: REST::Admin::AccountSerializer
  end

  def unsilence
    authorize @account, :unsilence?
    @account.unsilence!
    log_action :unsilence, @account
    render json: @account, serializer: REST::Admin::AccountSerializer
  end

  def unsuspend
    authorize @account, :unsuspend?
    @account.unsuspend!
    log_action :unsuspend, @account
    render json: @account, serializer: REST::Admin::AccountSerializer
  end

  private

  def set_accounts
    @accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
  end

  def set_account
    @account = Account.find(params[:id])
  end

  def filtered_accounts
    AccountFilter.new(filter_params).results
  end

  def filter_params
    params.permit(*FILTER_PARAMS)
  end

  def insert_pagination_headers
    set_pagination_headers(next_path, prev_path)
  end

  def next_path
    api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
  end

  def prev_path
    api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
  end

  def pagination_max_id
    @accounts.last.id
  end

  def pagination_since_id
    @accounts.first.id
  end

  def records_continue?
    @accounts.size == limit_param(LIMIT)
  end

  def pagination_params(core_params)
    params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
  end

  def require_local_account!
    forbidden unless @account.local? && @account.user.present?
  end
end

A app/controllers/api/v1/admin/reports_controller.rb => app/controllers/api/v1/admin/reports_controller.rb +108 -0
@@ 0,0 1,108 @@
# frozen_string_literal: true

class Api::V1::Admin::ReportsController < Api::BaseController
  include Authorization
  include AccountableConcern

  LIMIT = 100

  before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
  before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show]
  before_action :require_staff!
  before_action :set_reports, only: :index
  before_action :set_report, except: :index

  after_action :insert_pagination_headers, only: :index

  FILTER_PARAMS = %i(
    resolved
    account_id
    target_account_id
  ).freeze

  PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze

  def index
    authorize :report, :index?
    render json: @reports, each_serializer: REST::Admin::ReportSerializer
  end

  def show
    authorize @report, :show?
    render json: @report, serializer: REST::Admin::ReportSerializer
  end

  def assign_to_self
    authorize @report, :update?
    @report.update!(assigned_account_id: current_account.id)
    log_action :assigned_to_self, @report
    render json: @report, serializer: REST::Admin::ReportSerializer
  end

  def unassign
    authorize @report, :update?
    @report.update!(assigned_account_id: nil)
    log_action :unassigned, @report
    render json: @report, serializer: REST::Admin::ReportSerializer
  end

  def reopen
    authorize @report, :update?
    @report.unresolve!
    log_action :reopen, @report
    render json: @report, serializer: REST::Admin::ReportSerializer
  end

  def resolve
    authorize @report, :update?
    @report.resolve!(current_account)
    log_action :resolve, @report
    render json: @report, serializer: REST::Admin::ReportSerializer
  end

  private

  def set_reports
    @reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
  end

  def set_report
    @report = Report.find(params[:id])
  end

  def filtered_reports
    ReportFilter.new(filter_params).results
  end

  def filter_params
    params.permit(*FILTER_PARAMS)
  end

  def insert_pagination_headers
    set_pagination_headers(next_path, prev_path)
  end

  def next_path
    api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue?
  end

  def prev_path
    api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty?
  end

  def pagination_max_id
    @reports.last.id
  end

  def pagination_since_id
    @reports.first.id
  end

  def records_continue?
    @reports.size == limit_param(LIMIT)
  end

  def pagination_params(core_params)
    params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
  end
end

M app/controllers/media_controller.rb => app/controllers/media_controller.rb +10 -2
@@ 7,6 7,8 @@ class MediaController < ApplicationController

  before_action :set_media_attachment
  before_action :verify_permitted_status!
  before_action :check_playable, only: :player
  before_action :allow_iframing, only: :player

  content_security_policy only: :player do |p|
    p.frame_ancestors(false)


@@ 18,8 20,6 @@ class MediaController < ApplicationController

  def player
    @body_classes = 'player'
    response.headers['X-Frame-Options'] = 'ALLOWALL'
    raise ActiveRecord::RecordNotFound unless @media_attachment.video? || @media_attachment.gifv?
  end

  private


@@ 34,4 34,12 @@ class MediaController < ApplicationController
    # Reraise in order to get a 404 instead of a 403 error code
    raise ActiveRecord::RecordNotFound
  end

  def check_playable
    not_found unless @media_attachment.larger_media_format?
  end

  def allow_iframing
    response.headers['X-Frame-Options'] = 'ALLOWALL'
  end
end

M app/controllers/media_proxy_controller.rb => app/controllers/media_proxy_controller.rb +1 -1
@@ 39,6 39,6 @@ class MediaProxyController < ApplicationController
  end

  def reject_media?
    DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media?
    DomainBlock.reject_media?(@media_attachment.account.domain)
  end
end

M app/controllers/settings/identity_proofs_controller.rb => app/controllers/settings/identity_proofs_controller.rb +0 -4
@@ 61,8 61,4 @@ class Settings::IdentityProofsController < Settings::BaseController
  def post_params
    params.require(:account_identity_proof).permit(:post_status, :status_text)
  end

  def set_body_classes
    @body_classes = ''
  end
end

M app/javascript/flavours/glitch/components/media_gallery.js => app/javascript/flavours/glitch/components/media_gallery.js +1 -1
@@ 177,7 177,7 @@ class Item extends React.PureComponent {
    if (attachment.get('type') === 'unknown') {
      return (
        <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}>
          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
            <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
          </a>
        </div>

M app/javascript/flavours/glitch/components/status.js => app/javascript/flavours/glitch/components/status.js +7 -7
@@ 521,16 521,16 @@ export default class Status extends ImmutablePureComponent {
            media={status.get('media_attachments')}
          />
        );
      } else if (attachments.getIn([0, 'type']) === 'video') {  //  Media type is 'video'
        const video = status.getIn(['media_attachments', 0]);
      } else if (['video', 'audio'].includes(attachments.getIn([0, 'type']))) {
        const attachment = status.getIn(['media_attachments', 0]);

        media = (
          <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
            {Component => (<Component
              preview={video.get('preview_url')}
              blurhash={video.get('blurhash')}
              src={video.get('url')}
              alt={video.get('description')}
              preview={attachment.get('preview_url')}
              blurhash={attachment.get('blurhash')}
              src={attachment.get('url')}
              alt={attachment.get('description')}
              inline
              sensitive={status.get('sensitive')}
              letterbox={settings.getIn(['media', 'letterbox'])}


@@ 544,7 544,7 @@ export default class Status extends ImmutablePureComponent {
            />)}
          </Bundle>
        );
        mediaIcon = 'video-camera';
        mediaIcon = attachment.get('type') === 'video' ? 'video-camera' : 'music';
      } else {  //  Media type is 'image' or 'gifv'
        media = (
          <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>

M app/javascript/flavours/glitch/features/compose/containers/options_container.js => app/javascript/flavours/glitch/features/compose/containers/options_container.js +1 -1
@@ 16,7 16,7 @@ function mapStateToProps (state) {
    acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
    resetFileKey: state.getIn(['compose', 'resetFileKey']),
    hasPoll: !!poll,
    allowMedia: !poll && (media ? media.size < 4 && !media.some(item => item.get('type') === 'video') : true),
    allowMedia: !poll && (media ? media.size < 4 && !media.some(item => ['video', 'audio'].includes(item.get('type'))) : true),
    hasMedia: media && !!media.size,
    allowPoll: !(media && !!media.size),
    showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']),

M app/javascript/flavours/glitch/features/status/components/detailed_status.js => app/javascript/flavours/glitch/features/status/components/detailed_status.js +7 -7
@@ 131,14 131,14 @@ export default class DetailedStatus extends ImmutablePureComponent {
    } else if (status.get('media_attachments').size > 0) {
      if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
        media = <AttachmentList media={status.get('media_attachments')} />;
      } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
        const video = status.getIn(['media_attachments', 0]);
      } else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
        const attachment = status.getIn(['media_attachments', 0]);
        media = (
          <Video
            preview={video.get('preview_url')}
            blurhash={video.get('blurhash')}
            src={video.get('url')}
            alt={video.get('description')}
            preview={attachment.get('preview_url')}
            blurhash={attachment.get('blurhash')}
            src={attachment.get('url')}
            alt={attachment.get('description')}
            inline
            sensitive={status.get('sensitive')}
            letterbox={settings.getIn(['media', 'letterbox'])}


@@ 150,7 150,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
            onToggleVisibility={this.props.onToggleMediaVisibility}
          />
        );
        mediaIcon = 'video-camera';
        mediaIcon = attachment.get('type') === 'video' ? 'video-camera' : 'music';
      } else {
        media = (
          <MediaGallery

M app/javascript/flavours/glitch/styles/components/composer.scss => app/javascript/flavours/glitch/styles/components/composer.scss +1 -0
@@ 370,6 370,7 @@
    border-radius: 4px;
    height: 140px;
    width: 100%;
    background-color: $base-shadow-color;
    background-position: center;
    background-size: cover;
    background-repeat: no-repeat;

M app/javascript/mastodon/components/media_gallery.js => app/javascript/mastodon/components/media_gallery.js +1 -1
@@ 157,7 157,7 @@ class Item extends React.PureComponent {
    if (attachment.get('type') === 'unknown') {
      return (
        <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}>
          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
            <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
          </a>
        </div>

M app/javascript/mastodon/components/status.js => app/javascript/mastodon/components/status.js +6 -6
@@ 333,17 333,17 @@ class Status extends ImmutablePureComponent {
            media={status.get('media_attachments')}
          />
        );
      } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
        const video = status.getIn(['media_attachments', 0]);
      } else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
        const attachment = status.getIn(['media_attachments', 0]);

        media = (
          <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
            {Component => (
              <Component
                preview={video.get('preview_url')}
                blurhash={video.get('blurhash')}
                src={video.get('url')}
                alt={video.get('description')}
                preview={attachment.get('preview_url')}
                blurhash={attachment.get('blurhash')}
                src={attachment.get('url')}
                alt={attachment.get('description')}
                width={this.props.cachedMediaWidth}
                height={110}
                inline

M app/javascript/mastodon/features/compose/components/upload_button.js => app/javascript/mastodon/features/compose/components/upload_button.js +5 -3
@@ 7,9 7,11 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';

const messages = defineMessages({
  upload: { id: 'upload_button.label', defaultMessage: 'Add media (JPEG, PNG, GIF, WebM, MP4, MOV)' },
  upload: { id: 'upload_button.label', defaultMessage: 'Add media ({formats})' },
});

const SUPPORTED_FORMATS = 'JPEG, PNG, GIF, WebM, MP4, MOV, OGG, WAV, MP3, FLAC';

const makeMapStateToProps = () => {
  const mapStateToProps = state => ({
    acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),


@@ 60,9 62,9 @@ class UploadButton extends ImmutablePureComponent {

    return (
      <div className='compose-form__upload-button'>
        <IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
        <IconButton icon='paperclip' title={intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
        <label>
          <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span>
          <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })}</span>
          <input
            key={resetFileKey}
            ref={this.setRef}

M app/javascript/mastodon/features/compose/containers/upload_button_container.js => app/javascript/mastodon/features/compose/containers/upload_button_container.js +1 -1
@@ 3,7 3,7 @@ import UploadButton from '../components/upload_button';
import { uploadCompose } from '../../../actions/compose';

const mapStateToProps = state => ({
  disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
  disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
  unavailable: state.getIn(['compose', 'poll']) !== null,
  resetFileKey: state.getIn(['compose', 'resetFileKey']),
});

M app/javascript/mastodon/features/status/components/detailed_status.js => app/javascript/mastodon/features/status/components/detailed_status.js +6 -6
@@ 107,15 107,15 @@ export default class DetailedStatus extends ImmutablePureComponent {
    }

    if (status.get('media_attachments').size > 0) {
      if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
        const video = status.getIn(['media_attachments', 0]);
      if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
        const attachment = status.getIn(['media_attachments', 0]);

        media = (
          <Video
            preview={video.get('preview_url')}
            blurhash={video.get('blurhash')}
            src={video.get('url')}
            alt={video.get('description')}
            preview={attachment.get('preview_url')}
            blurhash={attachment.get('blurhash')}
            src={attachment.get('url')}
            alt={attachment.get('description')}
            width={300}
            height={150}
            inline

M app/javascript/mastodon/locales/ar.json => app/javascript/mastodon/locales/ar.json +1 -1
@@ 369,7 369,7 @@
  "trends.count_by_accounts": "{count} {rawCount, plural, one {person} آخرون {people}} يتحدثون",
  "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
  "upload_area.title": "اسحب ثم أفلت للرفع",
  "upload_button.label": "إضافة وسائط (JPEG، PNG، GIF، WebM، MP4، MOV)",
  "upload_button.label": "إضافة وسائط ({formats})",
  "upload_error.limit": "لقد تم بلوغ الحد الأقصى المسموح به لإرسال الملفات.",
  "upload_error.poll": "لا يمكن إدراج ملفات في استطلاعات الرأي.",
  "upload_form.description": "وصف للمعاقين بصريا",

M app/javascript/mastodon/locales/ca.json => app/javascript/mastodon/locales/ca.json +2 -2
@@ 314,7 314,7 @@
  "search_results.accounts": "Gent",
  "search_results.hashtags": "Etiquetes",
  "search_results.statuses": "Toots",
  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
  "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
  "status.admin_account": "Obre l'interfície de moderació per a @{name}",
  "status.admin_status": "Obre aquest toot a la interfície de moderació",
  "status.block": "Bloqueja @{name}",


@@ 366,7 366,7 @@
  "time_remaining.minutes": "{number, plural, one {# minut} other {# minuts}} restants",
  "time_remaining.moments": "Moments restants",
  "time_remaining.seconds": "{number, plural, one {# segon} other {# segons}} restants",
  "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
  "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {gent}} talking",
  "ui.beforeunload": "El teu esborrany es perdrà si surts de Mastodon.",
  "upload_area.title": "Arrossega i deixa anar per a carregar",
  "upload_button.label": "Afegir multimèdia (JPEG, PNG, GIF, WebM, MP4, MOV)",

M app/javascript/mastodon/locales/de.json => app/javascript/mastodon/locales/de.json +1 -1
@@ 369,7 369,7 @@
  "trends.count_by_accounts": "{count} {rawCount, plural, eine {Person} other {Personen}} reden darüber",
  "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.",
  "upload_area.title": "Zum Hochladen hereinziehen",
  "upload_button.label": "Mediendatei hinzufügen (JPEG, PNG, GIF, WebM, MP4, MOV)",
  "upload_button.label": "Mediendatei hinzufügen ({formats})",
  "upload_error.limit": "Dateiupload-Limit erreicht.",
  "upload_error.poll": "Dateiuploads sind in Kombination mit Umfragen nicht erlaubt.",
  "upload_form.description": "Für Menschen mit Sehbehinderung beschreiben",

M app/javascript/mastodon/locales/defaultMessages.json => app/javascript/mastodon/locales/defaultMessages.json +1 -1
@@ 1051,7 1051,7 @@
  {
    "descriptors": [
      {
        "defaultMessage": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)",
        "defaultMessage": "Add media ({formats})",
        "id": "upload_button.label"
      }
    ],

M app/javascript/mastodon/locales/en.json => app/javascript/mastodon/locales/en.json +1 -1
@@ 374,7 374,7 @@
  "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
  "upload_area.title": "Drag & drop to upload",
  "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)",
  "upload_button.label": "Add media ({formats})",
  "upload_error.limit": "File upload limit exceeded.",
  "upload_error.poll": "File upload not allowed with polls.",
  "upload_form.description": "Describe for the visually impaired",

M app/javascript/mastodon/locales/fi.json => app/javascript/mastodon/locales/fi.json +29 -29
@@ 71,20 71,20 @@
  "compose_form.lock_disclaimer": "Tilisi ei ole {locked}. Kuka tahansa voi seurata tiliäsi ja nähdä vain seuraajille rajaamasi julkaisut.",
  "compose_form.lock_disclaimer.lock": "lukittu",
  "compose_form.placeholder": "Mitä mietit?",
  "compose_form.poll.add_option": "Add a choice",
  "compose_form.poll.duration": "Poll duration",
  "compose_form.poll.option_placeholder": "Choice {number}",
  "compose_form.poll.remove_option": "Remove this choice",
  "compose_form.poll.add_option": "Lisää valinta",
  "compose_form.poll.duration": "Äänestyksen kesto",
  "compose_form.poll.option_placeholder": "Valinta numero",
  "compose_form.poll.remove_option": "Poista tämä valinta",
  "compose_form.publish": "Tuuttaa",
  "compose_form.publish_loud": "{publish}!",
  "compose_form.sensitive.hide": "Mark media as sensitive",
  "compose_form.publish_loud": "Julkista!",
  "compose_form.sensitive.hide": "Valitse tämä arkaluontoisena",
  "compose_form.sensitive.marked": "Media on merkitty arkaluontoiseksi",
  "compose_form.sensitive.unmarked": "Mediaa ei ole merkitty arkaluontoiseksi",
  "compose_form.spoiler.marked": "Teksti on piilotettu varoituksen taakse",
  "compose_form.spoiler.unmarked": "Teksti ei ole piilotettu",
  "compose_form.spoiler_placeholder": "Sisältövaroitus",
  "confirmation_modal.cancel": "Peruuta",
  "confirmations.block.block_and_report": "Block & Report",
  "confirmations.block.block_and_report": "Estä ja raportoi",
  "confirmations.block.confirm": "Estä",
  "confirmations.block.message": "Haluatko varmasti estää käyttäjän {name}?",
  "confirmations.delete.confirm": "Poista",


@@ 118,7 118,7 @@
  "emoji_button.symbols": "Symbolit",
  "emoji_button.travel": "Matkailu",
  "empty_column.account_timeline": "Ei ole 'toots' täällä!",
  "empty_column.account_unavailable": "Profile unavailable",
  "empty_column.account_unavailable": "Profiilia ei löydy",
  "empty_column.blocks": "Et ole vielä estänyt yhtään käyttäjää.",
  "empty_column.community": "Paikallinen aikajana on tyhjä. Homma lähtee käyntiin, kun kirjoitat jotain julkista!",
  "empty_column.direct": "Sinulla ei ole vielä yhtään viestiä yksittäiselle käyttäjälle. Kun lähetät tai vastaanotat sellaisen, se näkyy täällä.",


@@ 138,7 138,7 @@
  "follow_request.reject": "Hylkää",
  "getting_started.developers": "Kehittäjille",
  "getting_started.directory": "Profiili hakemisto",
  "getting_started.documentation": "Documentation",
  "getting_started.documentation": "Documentaatio",
  "getting_started.heading": "Aloitus",
  "getting_started.invite": "Kutsu ihmisiä",
  "getting_started.open_source_notice": "Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia GitHubissa: {github}.",


@@ 147,8 147,8 @@
  "hashtag.column_header.tag_mode.all": "ja {additional}",
  "hashtag.column_header.tag_mode.any": "tai {additional}",
  "hashtag.column_header.tag_mode.none": "ilman {additional}",
  "hashtag.column_settings.select.no_options_message": "No suggestions found",
  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
  "hashtag.column_settings.select.no_options_message": "Ehdostuta ei löydetty",
  "hashtag.column_settings.select.placeholder": "Laita häshtägejä…",
  "hashtag.column_settings.tag_mode.all": "Kaikki",
  "hashtag.column_settings.tag_mode.any": "Kaikki",
  "hashtag.column_settings.tag_mode.none": "Ei mikään",


@@ 156,25 156,25 @@
  "home.column_settings.basic": "Perusasetukset",
  "home.column_settings.show_reblogs": "Näytä buustaukset",
  "home.column_settings.show_replies": "Näytä vastaukset",
  "intervals.full.days": "{number, plural, one {# day} other {# days}}",
  "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
  "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
  "intervals.full.days": "Päivä päiviä",
  "intervals.full.hours": "Tunti tunteja",
  "intervals.full.minutes": "Minuuti minuuteja",
  "introduction.federation.action": "Seuraava",
  "introduction.federation.federated.headline": "Federated",
  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
  "introduction.federation.home.headline": "Home",
  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
  "introduction.federation.local.headline": "Local",
  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
  "introduction.interactions.action": "Finish toot-orial!",
  "introduction.interactions.favourite.headline": "Favourite",
  "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
  "introduction.interactions.reblog.headline": "Boost",
  "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
  "introduction.interactions.reply.headline": "Reply",
  "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
  "introduction.welcome.action": "Let's go!",
  "introduction.welcome.headline": "First steps",
  "introduction.federation.federated.headline": "Federaatioitettu",
  "introduction.federation.federated.text": "Julkisia viestejä muiden serverien that is not a word aikoo tulla federoituun aikajanaan.",
  "introduction.federation.home.headline": "Koti",
  "introduction.federation.home.text": "Viestit muilta pelaajilta jota seuraat aikovat tulla koti sivuusi. Voit seurata ketä vain missä vain serverillä!",
  "introduction.federation.local.headline": "Paikallinen",
  "introduction.federation.local.text": "Julkiset viestit muilta pelaajilta samalla serverillä tulevat sinun paikalliseen aikajanaan.",
  "introduction.interactions.action": "Suorita harjoitus!",
  "introduction.interactions.favourite.headline": "Lempi",
  "introduction.interactions.favourite.text": "Toot is not a word.",
  "introduction.interactions.reblog.headline": "Nopeutus",
  "introduction.interactions.reblog.text": "Toot is not a word",
  "introduction.interactions.reply.headline": "Vastaa",
  "introduction.interactions.reply.text": "TOOT IS NOT A WORD",
  "introduction.welcome.action": "Mennään!",
  "introduction.welcome.headline": "Ensimmäiset askeleet",
  "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
  "keyboard_shortcuts.back": "liiku taaksepäin",
  "keyboard_shortcuts.blocked": "avaa lista estetyistä käyttäjistä",

M app/javascript/mastodon/locales/it.json => app/javascript/mastodon/locales/it.json +28 -28
@@ 40,7 40,7 @@
  "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta",
  "bundle_column_error.body": "E' avvenuto un errore durante il caricamento di questo componente.",
  "bundle_column_error.retry": "Riprova",
  "bundle_column_error.title": "Network error",
  "bundle_column_error.title": "Errore di rete",
  "bundle_modal_error.close": "Chiudi",
  "bundle_modal_error.message": "C'è stato un errore mentre questo componente veniva caricato.",
  "bundle_modal_error.retry": "Riprova",


@@ 71,20 71,20 @@
  "compose_form.lock_disclaimer": "Il tuo account non è {bloccato}. Chiunque può decidere di seguirti per vedere i tuoi post per soli seguaci.",
  "compose_form.lock_disclaimer.lock": "bloccato",
  "compose_form.placeholder": "A cosa stai pensando?",
  "compose_form.poll.add_option": "Add a choice",
  "compose_form.poll.duration": "Poll duration",
  "compose_form.poll.option_placeholder": "Choice {number}",
  "compose_form.poll.remove_option": "Remove this choice",
  "compose_form.poll.add_option": "Aggiungi una scelta",
  "compose_form.poll.duration": "Durata del sondaggio",
  "compose_form.poll.option_placeholder": "Scelta {number}",
  "compose_form.poll.remove_option": "Rimuovi questa scelta",
  "compose_form.publish": "Toot",
  "compose_form.publish_loud": "{publish}!",
  "compose_form.sensitive.hide": "Mark media as sensitive",
  "compose_form.sensitive.hide": "Segna media come sensibile",
  "compose_form.sensitive.marked": "Questo media è contrassegnato come sensibile",
  "compose_form.sensitive.unmarked": "Questo media non è contrassegnato come sensibile",
  "compose_form.spoiler.marked": "Il testo è nascosto dall'avviso",
  "compose_form.spoiler.unmarked": "Il testo non è nascosto",
  "compose_form.spoiler_placeholder": "Content warning",
  "confirmation_modal.cancel": "Annulla",
  "confirmations.block.block_and_report": "Block & Report",
  "confirmations.block.block_and_report": "Blocca & Segnala",
  "confirmations.block.confirm": "Blocca",
  "confirmations.block.message": "Sei sicuro di voler bloccare {name}?",
  "confirmations.delete.confirm": "Cancella",


@@ 118,7 118,7 @@
  "emoji_button.symbols": "Simboli",
  "emoji_button.travel": "Viaggi e luoghi",
  "empty_column.account_timeline": "Non ci sono toot qui!",
  "empty_column.account_unavailable": "Profile unavailable",
  "empty_column.account_unavailable": "Profilo non disponibile",
  "empty_column.blocks": "Non hai ancora bloccato nessun utente.",
  "empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!",
  "empty_column.direct": "Non hai ancora nessun messaggio diretto. Quando ne manderai o riceverai qualcuno, apparirà qui.",


@@ 156,15 156,15 @@
  "home.column_settings.basic": "Semplice",
  "home.column_settings.show_reblogs": "Mostra post condivisi",
  "home.column_settings.show_replies": "Mostra risposte",
  "intervals.full.days": "{number, plural, one {# day} other {# days}}",
  "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
  "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
  "intervals.full.days": "{number, plural, one {# giorno} other {# giorni}}",
  "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}",
  "intervals.full.minutes": "{number, plural, one {# minuto} other {# minuti}}",
  "introduction.federation.action": "Avanti",
  "introduction.federation.federated.headline": "Federated",
  "introduction.federation.federated.headline": "Federato",
  "introduction.federation.federated.text": "I post pubblici provenienti da altri server del fediverse saranno mostrati nella timeline federata.",
  "introduction.federation.home.headline": "Home",
  "introduction.federation.home.text": "I post scritti da persone che segui saranno mostrati nella timeline home. Puoi seguire chiunque su qualunque server!",
  "introduction.federation.local.headline": "Local",
  "introduction.federation.local.headline": "Locale",
  "introduction.federation.local.text": "I post pubblici scritti da persone sul tuo stesso server saranno mostrati nella timeline locale.",
  "introduction.interactions.action": "Finisci il tutorial!",
  "introduction.interactions.favourite.headline": "Apprezza",


@@ 204,17 204,17 @@
  "keyboard_shortcuts.search": "per spostare il focus sulla ricerca",
  "keyboard_shortcuts.start": "per aprire la colonna \"Come iniziare\"",
  "keyboard_shortcuts.toggle_hidden": "per mostrare/nascondere il testo dei CW",
  "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
  "keyboard_shortcuts.toggle_sensitivity": "mostrare/nascondere media",
  "keyboard_shortcuts.toot": "per iniziare a scrivere un toot completamente nuovo",
  "keyboard_shortcuts.unfocus": "per uscire dall'area di composizione o dalla ricerca",
  "keyboard_shortcuts.up": "per spostarsi in alto nella lista",
  "lightbox.close": "Chiudi",
  "lightbox.next": "Successivo",
  "lightbox.previous": "Precedente",
  "lightbox.view_context": "View context",
  "lightbox.view_context": "Mostra contesto",
  "lists.account.add": "Aggiungi alla lista",
  "lists.account.remove": "Togli dalla lista",
  "lists.delete": "Delete list",
  "lists.delete": "Elimina lista",
  "lists.edit": "Modifica lista",
  "lists.edit.submit": "Cambia titolo",
  "lists.new.create": "Aggiungi lista",


@@ 243,16 243,16 @@
  "navigation_bar.lists": "Liste",
  "navigation_bar.logout": "Esci",
  "navigation_bar.mutes": "Utenti silenziati",
  "navigation_bar.personal": "Personal",
  "navigation_bar.personal": "Personale",
  "navigation_bar.pins": "Toot fissati in cima",
  "navigation_bar.preferences": "Impostazioni",
  "navigation_bar.profile_directory": "Profile directory",
  "navigation_bar.profile_directory": "Directory dei profili",
  "navigation_bar.public_timeline": "Timeline federata",
  "navigation_bar.security": "Sicurezza",
  "notification.favourite": "{name} ha apprezzato il tuo post",
  "notification.follow": "{name} ha iniziato a seguirti",
  "notification.mention": "{name} ti ha menzionato",
  "notification.poll": "A poll you have voted in has ended",
  "notification.poll": "Un sondaggio in cui hai votato è terminato",
  "notification.reblog": "{name} ha condiviso il tuo post",
  "notifications.clear": "Cancella notifiche",
  "notifications.clear_confirmation": "Vuoi davvero cancellare tutte le notifiche?",


@@ 263,7 263,7 @@
  "notifications.column_settings.filter_bar.show": "Mostra",
  "notifications.column_settings.follow": "Nuovi seguaci:",
  "notifications.column_settings.mention": "Menzioni:",
  "notifications.column_settings.poll": "Poll results:",
  "notifications.column_settings.poll": "Risultati del sondaggio:",
  "notifications.column_settings.push": "Notifiche push",
  "notifications.column_settings.reblog": "Post condivisi:",
  "notifications.column_settings.show": "Mostra in colonna",


@@ 273,14 273,14 @@
  "notifications.filter.favourites": "Apprezzati",
  "notifications.filter.follows": "Seguaci",
  "notifications.filter.mentions": "Menzioni",
  "notifications.filter.polls": "Poll results",
  "notifications.filter.polls": "Risultati del sondaggio",
  "notifications.group": "{count} notifiche",
  "poll.closed": "Chiuso",
  "poll.refresh": "Aggiorna",
  "poll.total_votes": "{count, plural, one {# voto} other {# voti}}",
  "poll.vote": "Vota",
  "poll_button.add_poll": "Add a poll",
  "poll_button.remove_poll": "Remove poll",
  "poll_button.add_poll": "Aggiungi un sondaggio",
  "poll_button.remove_poll": "Rimuovi sondaggio",
  "privacy.change": "Modifica privacy del post",
  "privacy.direct.long": "Invia solo a utenti menzionati",
  "privacy.direct.short": "Diretto",


@@ 292,8 292,8 @@
  "privacy.unlisted.short": "Non elencato",
  "regeneration_indicator.label": "Caricamento in corso…",
  "regeneration_indicator.sublabel": "Stiamo preparando il tuo home feed!",
  "relative_time.days": "{number}d",
  "relative_time.hours": "{number}h",
  "relative_time.days": "{number}g",
  "relative_time.hours": "{number}o",
  "relative_time.just_now": "ora",
  "relative_time.minutes": "{number}m",
  "relative_time.seconds": "{number}s",


@@ 307,8 307,8 @@
  "search.placeholder": "Cerca",
  "search_popout.search_format": "Formato di ricerca avanzato",
  "search_popout.tips.full_text": "Testo semplice per trovare gli status che hai scritto, segnato come apprezzati, condiviso o in cui sei stato citato, e inoltre i nomi utente, nomi visualizzati e hashtag che lo contengono.",
  "search_popout.tips.hashtag": "hashtag",
  "search_popout.tips.status": "status",
  "search_popout.tips.hashtag": "etichetta",
  "search_popout.tips.status": "stato",
  "search_popout.tips.text": "Testo semplice per trovare nomi visualizzati, nomi utente e hashtag che lo contengono",
  "search_popout.tips.user": "utente",
  "search_results.accounts": "Gente",


@@ 371,7 371,7 @@
  "upload_area.title": "Trascina per caricare",
  "upload_button.label": "Aggiungi file multimediale",
  "upload_error.limit": "Limite al caricamento di file superato.",
  "upload_error.poll": "File upload not allowed with polls.",
  "upload_error.poll": "Caricamento file non consentito nei sondaggi.",
  "upload_form.description": "Descrizione per utenti con disabilità visive",
  "upload_form.focus": "Modifica anteprima",
  "upload_form.undo": "Cancella",

M app/javascript/mastodon/locales/ja.json => app/javascript/mastodon/locales/ja.json +1 -1
@@ 374,7 374,7 @@
  "trends.count_by_accounts": "{count}人がトゥート",
  "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
  "upload_area.title": "ドラッグ&ドロップでアップロード",
  "upload_button.label": "メディアを追加 (JPEG, PNG, GIF, WebM, MP4, MOV)",
  "upload_button.label": "メディアを追加 ({formats})",
  "upload_error.limit": "アップロードできる上限を超えています。",
  "upload_error.poll": "アンケートではファイルをアップロードできません。",
  "upload_form.description": "視覚障害者のための説明",

M app/javascript/mastodon/locales/nl.json => app/javascript/mastodon/locales/nl.json +6 -6
@@ 361,15 361,15 @@
  "tabs_bar.local_timeline": "Lokaal",
  "tabs_bar.notifications": "Meldingen",
  "tabs_bar.search": "Zoeken",
  "time_remaining.days": "{number, plural, one {# dag} other {# dagen}} left",
  "time_remaining.hours": "{number, plural, one {# uur} other {# uur}} left",
  "time_remaining.minutes": "{number, plural, one {# minuut} other {# minuten}} left",
  "time_remaining.days": "{number, plural, one {# dag} other {# dagen}} te gaan",
  "time_remaining.hours": "{number, plural, one {# uur} other {# uur}} te gaan",
  "time_remaining.minutes": "{number, plural, one {# minuut} other {# minuten}} te gaan",
  "time_remaining.moments": "Nog enkele ogenblikken resterend",
  "time_remaining.seconds": "{number, plural, one {# seconde} other {# seconden}} left",
  "time_remaining.seconds": "{number, plural, one {# seconde} other {# seconden}} te gaan",
  "trends.count_by_accounts": "{count} {rawCount, plural, one {persoon praat} other {mensen praten}} hierover",
  "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.",
  "upload_area.title": "Hierin slepen om te uploaden",
  "upload_button.label": "Media toevoegen (JPEG, PNG, GIF, WebM, MP4, MOV)",
  "upload_area.title": "Hiernaar toe slepen om te uploaden",
  "upload_button.label": "Media toevoegen ({formats})",
  "upload_error.limit": "Uploadlimiet van bestand overschreden.",
  "upload_error.poll": "Het uploaden van bestanden is in polls niet toegestaan.",
  "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking",

M app/javascript/mastodon/locales/sk.json => app/javascript/mastodon/locales/sk.json +1 -1
@@ 1,5 1,5 @@
{
  "account.add_or_remove_from_list": "Pridaj, alebo odstráň zo zoznamov",
  "account.add_or_remove_from_list": "Pridaj do, alebo odober zo zoznamov",
  "account.badges.bot": "Bot",
  "account.block": "Blokuj @{name}",
  "account.block_domain": "Ukry všetko z {domain}",

M app/javascript/mastodon/locales/sl.json => app/javascript/mastodon/locales/sl.json +173 -173
@@ 149,10 149,10 @@
  "hashtag.column_header.tag_mode.none": "brez {additional}",
  "hashtag.column_settings.select.no_options_message": "Ni najdenih predlogov",
  "hashtag.column_settings.select.placeholder": "Vpiši ključnik…",
  "hashtag.column_settings.tag_mode.all": "Vse našteto",
  "hashtag.column_settings.tag_mode.all": "Vse od naštetega",
  "hashtag.column_settings.tag_mode.any": "Karkoli od naštetega",
  "hashtag.column_settings.tag_mode.none": "Nič od naštetega",
  "hashtag.column_settings.tag_toggle": "V ta stolpec vključite dodatne oznake",
  "hashtag.column_settings.tag_toggle": "Za ta stolpec vključi dodatne oznake",
  "home.column_settings.basic": "Osnovno",
  "home.column_settings.show_reblogs": "Pokaži spodbude",
  "home.column_settings.show_replies": "Pokaži odgovore",


@@ 161,187 161,187 @@
  "intervals.full.minutes": "{number, plural, one {# minuta} two {# minuti} few {# minute} other {# minut}}",
  "introduction.federation.action": "Naprej",
  "introduction.federation.federated.headline": "Združeno",
  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
  "introduction.federation.home.headline": "Home",
  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
  "introduction.federation.local.headline": "Local",
  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
  "introduction.interactions.action": "Finish toot-orial!",
  "introduction.interactions.favourite.headline": "Favourite",
  "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
  "introduction.interactions.reblog.headline": "Boost",
  "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
  "introduction.interactions.reply.headline": "Reply",
  "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
  "introduction.welcome.action": "Let's go!",
  "introduction.welcome.headline": "First steps",
  "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
  "keyboard_shortcuts.back": "za krmarjenje nazaj",
  "keyboard_shortcuts.blocked": "to open blocked users list",
  "keyboard_shortcuts.boost": "suniti",
  "keyboard_shortcuts.column": "osredotočiti status v enega od stolpcev",
  "keyboard_shortcuts.compose": "osredotočiti na sestavljanje besedila",
  "introduction.federation.federated.text": "Javne objave iz drugih strežnikov fediverse-a bodo prikazane v združeni časovnici.",
  "introduction.federation.home.headline": "Domov",
  "introduction.federation.home.text": "Objave oseb, ki jim sledite, bodo prikazane v vaši domači časovnici. Lahko sledite vsakomur na katerem koli strežniku!",
  "introduction.federation.local.headline": "Lokalno",
  "introduction.federation.local.text": "Javne objave ljudi na istem strežniku, se bodo prikazale na lokalni časovnici.",
  "introduction.interactions.action": "Zaključi vadnico!",
  "introduction.interactions.favourite.headline": "Priljubljeni",
  "introduction.interactions.favourite.text": "Tut lahko shranite za pozneje in ga vzljubite ter s tem pokažete avtorju, da vam je ta tut priljubljen.",
  "introduction.interactions.reblog.headline": "Spodbudi",
  "introduction.interactions.reblog.text": "Tute drugih ljudi lahko delite z vašimi sledilci, tako da spodbudite tute.",
  "introduction.interactions.reply.headline": "Odgovori",
  "introduction.interactions.reply.text": "Lahko odgovarjate na tuje in vaše tute, kar bo odgovore povezalo v pogovor.",
  "introduction.welcome.action": "Gremo!",
  "introduction.welcome.headline": "Prvi koraki",
  "introduction.welcome.text": "Dobrodošli v fediverse-u! Čez nekaj trenutkov boste lahko oddajali sporočila in se pogovarjali s prijatelji prek različnih strežnikov. Vendar je ta strežnik {domain} poseben - gosti vaš profil, zato si zapomnite njegovo ime.",
  "keyboard_shortcuts.back": "pojdi nazaj",
  "keyboard_shortcuts.blocked": "odpri seznam blokiranih uporabnikov",
  "keyboard_shortcuts.boost": "spodbudi",
  "keyboard_shortcuts.column": "fokusiraj na status v enemu od stolpcev",
  "keyboard_shortcuts.compose": "fokusiraj na območje za sestavljanje besedila",
  "keyboard_shortcuts.description": "Opis",
  "keyboard_shortcuts.direct": "to open direct messages column",
  "keyboard_shortcuts.down": "premakniti navzdol po seznamu",
  "keyboard_shortcuts.enter": "odpreti status",
  "keyboard_shortcuts.favourite": "to favourite",
  "keyboard_shortcuts.favourites": "to open favourites list",
  "keyboard_shortcuts.federated": "to open federated timeline",
  "keyboard_shortcuts.direct": "odpri stolpec za neposredna sporočila",
  "keyboard_shortcuts.down": "premakni se navzdol po seznamu",
  "keyboard_shortcuts.enter": "odpri status",
  "keyboard_shortcuts.favourite": "vzljubi",
  "keyboard_shortcuts.favourites": "odpri seznam priljubljenih",
  "keyboard_shortcuts.federated": "odpri združeno časovnico",
  "keyboard_shortcuts.heading": "Tipkovne bližnjice",
  "keyboard_shortcuts.home": "to open home timeline",
  "keyboard_shortcuts.home": "odpri domačo časovnico",
  "keyboard_shortcuts.hotkey": "Hitra tipka",
  "keyboard_shortcuts.legend": "to display this legend",
  "keyboard_shortcuts.local": "to open local timeline",
  "keyboard_shortcuts.mention": "to mention author",
  "keyboard_shortcuts.muted": "to open muted users list",
  "keyboard_shortcuts.my_profile": "to open your profile",
  "keyboard_shortcuts.notifications": "to open notifications column",
  "keyboard_shortcuts.pinned": "to open pinned toots list",
  "keyboard_shortcuts.profile": "to open author's profile",
  "keyboard_shortcuts.reply": "to reply",
  "keyboard_shortcuts.requests": "to open follow requests list",
  "keyboard_shortcuts.search": "to focus search",
  "keyboard_shortcuts.start": "to open \"get started\" column",
  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
  "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
  "keyboard_shortcuts.toot": "da začnete povsem nov tut",
  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
  "keyboard_shortcuts.up": "to move up in the list",
  "lightbox.close": "Close",
  "lightbox.next": "Next",
  "lightbox.previous": "Previous",
  "lightbox.view_context": "View context",
  "lists.account.add": "Add to list",
  "lists.account.remove": "Remove from list",
  "lists.delete": "Delete list",
  "lists.edit": "Edit list",
  "lists.edit.submit": "Change title",
  "lists.new.create": "Add list",
  "lists.new.title_placeholder": "New list title",
  "lists.search": "Search among people you follow",
  "lists.subheading": "Your lists",
  "loading_indicator.label": "Loading...",
  "media_gallery.toggle_visible": "Toggle visibility",
  "missing_indicator.label": "Not found",
  "missing_indicator.sublabel": "This resource could not be found",
  "mute_modal.hide_notifications": "Hide notifications from this user?",
  "navigation_bar.apps": "Mobile apps",
  "navigation_bar.blocks": "Blocked users",
  "navigation_bar.community_timeline": "Local timeline",
  "navigation_bar.compose": "Compose new toot",
  "navigation_bar.direct": "Direct messages",
  "navigation_bar.discover": "Discover",
  "navigation_bar.domain_blocks": "Hidden domains",
  "navigation_bar.edit_profile": "Edit profile",
  "navigation_bar.favourites": "Favourites",
  "navigation_bar.filters": "Muted words",
  "navigation_bar.follow_requests": "Follow requests",
  "navigation_bar.follows_and_followers": "Follows and followers",
  "navigation_bar.info": "O tem vozlišču",
  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
  "navigation_bar.lists": "Lists",
  "navigation_bar.logout": "Logout",
  "navigation_bar.mutes": "Muted users",
  "navigation_bar.personal": "Personal",
  "keyboard_shortcuts.legend": "pokaži to legendo",
  "keyboard_shortcuts.local": "odpri lokalno časovnico",
  "keyboard_shortcuts.mention": "omeni avtorja",
  "keyboard_shortcuts.muted": "odpri seznam utišanih uporabnikov",
  "keyboard_shortcuts.my_profile": "odpri svoj profil",
  "keyboard_shortcuts.notifications": "odpri stolpec z obvestili",
  "keyboard_shortcuts.pinned": "odpri seznam pripetih tutov",
  "keyboard_shortcuts.profile": "odpri avtorjev profil",
  "keyboard_shortcuts.reply": "odgovori",
  "keyboard_shortcuts.requests": "odpri seznam s prošnjami za sledenje",
  "keyboard_shortcuts.search": "fokusiraj na iskanje",
  "keyboard_shortcuts.start": "odpri stolpec \"začni\"",
  "keyboard_shortcuts.toggle_hidden": "prikaži/skrij besedilo za CW",
  "keyboard_shortcuts.toggle_sensitivity": "prikaži/skrij medije",
  "keyboard_shortcuts.toot": "začni povsem nov tut",
  "keyboard_shortcuts.unfocus": "odfokusiraj območje za sestavljanje besedila/iskanje",
  "keyboard_shortcuts.up": "premakni se navzgor po seznamu",
  "lightbox.close": "Zapri",
  "lightbox.next": "Naslednji",
  "lightbox.previous": "Prejšnji",
  "lightbox.view_context": "Poglej kontekst",
  "lists.account.add": "Dodaj na seznam",
  "lists.account.remove": "Odstrani s seznama",
  "lists.delete": "Izbriši seznam",
  "lists.edit": "Uredi seznam",
  "lists.edit.submit": "Spremeni naslov",
  "lists.new.create": "Dodaj seznam",
  "lists.new.title_placeholder": "Nov naslov seznama",
  "lists.search": "Išči med ljudmi, katerim sledite",
  "lists.subheading": "Vaši seznami",
  "loading_indicator.label": "Nalaganje...",
  "media_gallery.toggle_visible": "Preklopi vidljivost",
  "missing_indicator.label": "Ni najdeno",
  "missing_indicator.sublabel": "Tega vira ni bilo mogoče najti",
  "mute_modal.hide_notifications": "Skrij obvestila tega uporabnika?",
  "navigation_bar.apps": "Mobilne aplikacije",
  "navigation_bar.blocks": "Blokirani uporabniki",
  "navigation_bar.community_timeline": "Lokalna časovnica",
  "navigation_bar.compose": "Sestavi nov tut",
  "navigation_bar.direct": "Neposredna sporočila",
  "navigation_bar.discover": "Odkrijte",
  "navigation_bar.domain_blocks": "Skrite domene",
  "navigation_bar.edit_profile": "Uredi profil",
  "navigation_bar.favourites": "Priljubljeni",
  "navigation_bar.filters": "Utišane besede",
  "navigation_bar.follow_requests": "Prošnje za sledenje",
  "navigation_bar.follows_and_followers": "Sledenja in sledilci",
  "navigation_bar.info": "O tem strežniku",
  "navigation_bar.keyboard_shortcuts": "Hitre tipke",
  "navigation_bar.lists": "Seznami",
  "navigation_bar.logout": "Odjava",
  "navigation_bar.mutes": "Utišani uporabniki",
  "navigation_bar.personal": "Osebno",
  "navigation_bar.pins": "Pripeti tuti",
  "navigation_bar.preferences": "Preferences",
  "navigation_bar.profile_directory": "Profile directory",
  "navigation_bar.public_timeline": "Federated timeline",
  "navigation_bar.security": "Security",
  "notification.favourite": "{name} favourited your status",
  "notification.follow": "{name} followed you",
  "notification.mention": "{name} mentioned you",
  "notification.poll": "A poll you have voted in has ended",
  "notification.reblog": "{name} boosted your status",
  "notifications.clear": "Clear notifications",
  "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
  "notifications.column_settings.alert": "Desktop notifications",
  "notifications.column_settings.favourite": "Favourites:",
  "notifications.column_settings.filter_bar.advanced": "Display all categories",
  "notifications.column_settings.filter_bar.category": "Quick filter bar",
  "notifications.column_settings.filter_bar.show": "Show",
  "notifications.column_settings.follow": "New followers:",
  "notifications.column_settings.mention": "Mentions:",
  "notifications.column_settings.poll": "Poll results:",
  "notifications.column_settings.push": "Push notifications",
  "notifications.column_settings.reblog": "Boosts:",
  "notifications.column_settings.show": "Show in column",
  "notifications.column_settings.sound": "Play sound",
  "notifications.filter.all": "All",
  "notifications.filter.boosts": "Boosts",
  "notifications.filter.favourites": "Favourites",
  "notifications.filter.follows": "Follows",
  "notifications.filter.mentions": "Mentions",
  "notifications.filter.polls": "Poll results",
  "notifications.group": "{count} notifications",
  "poll.closed": "Closed",
  "poll.refresh": "Refresh",
  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
  "poll.vote": "Vote",
  "poll_button.add_poll": "Add a poll",
  "poll_button.remove_poll": "Remove poll",
  "privacy.change": "Adjust status privacy",
  "privacy.direct.long": "Post to mentioned users only",
  "privacy.direct.short": "Direct",
  "privacy.private.long": "Post to followers only",
  "privacy.private.short": "Followers-only",
  "privacy.public.long": "Post to public timelines",
  "privacy.public.short": "Public",
  "privacy.unlisted.long": "Do not show in public timelines",
  "privacy.unlisted.short": "Unlisted",
  "regeneration_indicator.label": "Loading…",
  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
  "navigation_bar.preferences": "Nastavitve",
  "navigation_bar.profile_directory": "Imenik profilov",
  "navigation_bar.public_timeline": "Združena časovnica",
  "navigation_bar.security": "Varnost",
  "notification.favourite": "{name} je vzljubil/a vaš status",
  "notification.follow": "{name} vam sledi",
  "notification.mention": "{name} vas je omenil/a",
  "notification.poll": "Glasovanje, v katerem ste sodelovali, se je končalo",
  "notification.reblog": "{name} je spodbudil/a vaš status",
  "notifications.clear": "Počisti obvestila",
  "notifications.clear_confirmation": "Ali ste prepričani, da želite trajno izbrisati vsa vaša obvestila?",
  "notifications.column_settings.alert": "Namizna obvestila",
  "notifications.column_settings.favourite": "Priljubljeni:",
  "notifications.column_settings.filter_bar.advanced": "Prikaži vse kategorije",
  "notifications.column_settings.filter_bar.category": "Vrstica za hitro filtriranje",
  "notifications.column_settings.filter_bar.show": "Pokaži",
  "notifications.column_settings.follow": "Novi sledilci:",
  "notifications.column_settings.mention": "Omembe:",
  "notifications.column_settings.poll": "Rezultati glasovanja:",
  "notifications.column_settings.push": "Potisna obvestila",
  "notifications.column_settings.reblog": "Spodbude:",
  "notifications.column_settings.show": "Prikaži v stolpcu",
  "notifications.column_settings.sound": "Predvajaj zvok",
  "notifications.filter.all": "Vse",
  "notifications.filter.boosts": "Spodbude",
  "notifications.filter.favourites": "Priljubljeni",
  "notifications.filter.follows": "Sledi",
  "notifications.filter.mentions": "Omembe",
  "notifications.filter.polls": "Rezultati glasovanj",
  "notifications.group": "{count} obvestil",
  "poll.closed": "Zaprto",
  "poll.refresh": "Osveži",
  "poll.total_votes": "{count, plural,one {# glas} other {# glasov}}",
  "poll.vote": "Glasuj",
  "poll_button.add_poll": "Dodaj anketo",
  "poll_button.remove_poll": "Odstrani anketo",
  "privacy.change": "Prilagodi zasebnost statusa",
  "privacy.direct.long": "Objavi samo omenjenim uporabnikom",
  "privacy.direct.short": "Neposredno",
  "privacy.private.long": "Objavi samo sledilcem",
  "privacy.private.short": "Samo sledilci",
  "privacy.public.long": "Objavi na javne časovnice",
  "privacy.public.short": "Javno",
  "privacy.unlisted.long": "Ne objavi na javne časovnice",
  "privacy.unlisted.short": "Ni prikazano",
  "regeneration_indicator.label": "Nalaganje…",
  "regeneration_indicator.sublabel": "Vaš domači vir se pripravlja!",
  "relative_time.days": "{number}d",
  "relative_time.hours": "{number}h",
  "relative_time.just_now": "now",
  "relative_time.just_now": "zdaj",
  "relative_time.minutes": "{number}m",
  "relative_time.seconds": "{number}s",
  "reply_indicator.cancel": "Cancel",
  "report.forward": "Forward to {target}",
  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
  "report.placeholder": "Additional comments",
  "report.submit": "Submit",
  "report.target": "Report {target}",
  "search.placeholder": "Search",
  "search_popout.search_format": "Advanced search format",
  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
  "search_popout.tips.hashtag": "hashtag",
  "search_popout.tips.status": "status",
  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
  "search_popout.tips.user": "user",
  "search_results.accounts": "People",
  "search_results.hashtags": "Hashtags",
  "reply_indicator.cancel": "Prekliči",
  "report.forward": "Posreduj do {target}",
  "report.forward_hint": "Račun je iz drugega strežnika. Pošljem anonimno kopijo poročila tudi na drugi strežnik?",
  "report.hint": "Poročilo bo poslano moderatorjem vašega vozlišča. Spodaj lahko navedete, zakaj prijavljate ta račun:",
  "report.placeholder": "Dodatni komentarji",
  "report.submit": "Pošlji",
  "report.target": "Prijavi {target}",
  "search.placeholder": "Iskanje",
  "search_popout.search_format": "Napredna oblika iskanja",
  "search_popout.tips.full_text": "Enostavno besedilo vrne statuse, ki ste jih napisali, vzljubili, spodbudili ali ste bili v njih omenjeni, kot tudi ujemajoča se uporabniška imena, prikazna imena in ključnike.",
  "search_popout.tips.hashtag": "ključnik",
  "search_popout.tips.status": "stanje",
  "search_popout.tips.text": "Enostavno besedilo vrne ujemajoča se prikazna imena, uporabniška imena in ključnike",
  "search_popout.tips.user": "uporabnik",
  "search_results.accounts": "Ljudje",
  "search_results.hashtags": "Ključniki",
  "search_results.statuses": "Tuti",
  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
  "status.admin_account": "Open moderation interface for @{name}",
  "status.admin_status": "Open this status in the moderation interface",
  "status.block": "Block @{name}",
  "status.cancel_reblog_private": "Unboost",
  "status.cannot_reblog": "This post cannot be boosted",
  "status.copy": "Copy link to status",
  "status.delete": "Delete",
  "status.detailed_status": "Detailed conversation view",
  "status.direct": "Direct message @{name}",
  "status.embed": "Embed",
  "status.favourite": "Favourite",
  "status.filtered": "Filtered",
  "status.load_more": "Load more",
  "status.media_hidden": "Media hidden",
  "status.mention": "Mention @{name}",
  "status.more": "More",
  "status.mute": "Mute @{name}",
  "status.mute_conversation": "Mute conversation",
  "status.open": "Expand this status",
  "status.pin": "Pin on profile",
  "search_results.total": "{count, number} {count, plural, one {rezultat} other {rezultatov}}",
  "status.admin_account": "Odpri vmesnik za moderiranje za @{name}",
  "status.admin_status": "Odpri status v vmesniku za moderiranje",
  "status.block": "Blokiraj @{name}",
  "status.cancel_reblog_private": "Prekini spodbudo",
  "status.cannot_reblog": "Te objave ni mogoče spodbuditi",
  "status.copy": "Kopiraj povezavo do statusa",
  "status.delete": "Izbriši",
  "status.detailed_status": "Podroben pogled pogovora",
  "status.direct": "Neposredno sporočilo @{name}",
  "status.embed": "Vgradi",
  "status.favourite": "Priljubljen",
  "status.filtered": "Filtrirano",
  "status.load_more": "Naloži več",
  "status.media_hidden": "Mediji so skriti",
  "status.mention": "Omeni @{name}",
  "status.more": "Več",
  "status.mute": "Utišaj @{name}",
  "status.mute_conversation": "Utišaj pogovor",
  "status.open": "Razširi ta status",
  "status.pin": "Pripni na profil",
  "status.pinned": "Pripeti tut",
  "status.read_more": "Read more",
  "status.reblog": "Suni",
  "status.reblog_private": "Suni v prvotno občinstvo",
  "status.reblogged_by": "{name} sunjen",
  "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
  "status.redraft": "Delete & re-draft",
  "status.read_more": "Preberi več",
  "status.reblog": "Spodbudi",
  "status.reblog_private": "Spodbudi izvirnemu občinstvu",
  "status.reblogged_by": "{name} spodbujen",
  "status.reblogs.empty": "Nihče še ni spodbudil tega tuta. Ko se bo to zgodilo, se bodo pojavili tukaj.",
  "status.redraft": "Izbriši in preoblikuj",
  "status.reply": "Odgovori",
  "status.replyAll": "Odgovori na objavo",
  "status.report": "Prijavi @{name}",

M app/javascript/mastodon/locales/zh-CN.json => app/javascript/mastodon/locales/zh-CN.json +1 -1
@@ 320,7 320,7 @@
  "status.block": "屏蔽 @{name}",
  "status.cancel_reblog_private": "取消转嘟",
  "status.cannot_reblog": "无法转嘟这条嘟文",
  "status.copy": "复制链接到嘟文中",
  "status.copy": "复制嘟文链接",
  "status.delete": "删除",
  "status.detailed_status": "对话详情",
  "status.direct": "发送私信给 @{name}",

M app/javascript/styles/mastodon/components.scss => app/javascript/styles/mastodon/components.scss +1 -0
@@ 557,6 557,7 @@

    .compose-form__upload-thumbnail {
      border-radius: 4px;
      background-color: $base-shadow-color;
      background-position: center;
      background-size: cover;
      background-repeat: no-repeat;

M app/lib/activitypub/activity/create.rb => app/lib/activitypub/activity/create.rb +2 -2
@@ 370,7 370,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
  end

  def unsupported_media_type?(mime_type)
    mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
    mime_type.present? && !MediaAttachment.supported_mime_types.include?(mime_type)
  end

  def supported_blurhash?(blurhash)


@@ 380,7 380,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity

  def skip_download?
    return @skip_download if defined?(@skip_download)
    @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
    @skip_download ||= DomainBlock.reject_media?(@account.domain)
  end

  def reply_to_local?

M app/lib/activitypub/activity/flag.rb => app/lib/activitypub/activity/flag.rb +1 -1
@@ 23,7 23,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
  private

  def skip_reports?
    DomainBlock.find_by(domain: @account.domain)&.reject_reports?
    DomainBlock.reject_reports?(@account.domain)
  end

  def object_uris

M app/lib/ostatus/activity/creation.rb => app/lib/ostatus/activity/creation.rb +2 -2
@@ 148,7 148,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
  end

  def save_media
    do_not_download = DomainBlock.find_by(domain: @account.domain)&.reject_media?
    do_not_download = DomainBlock.reject_media?(@account.domain)
    media_attachments = []

    @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link|


@@ 176,7 176,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
  end

  def save_emojis(parent)
    do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
    do_not_download = DomainBlock.reject_media?(parent.account.domain)

    return if do_not_download


M app/models/account.rb => app/models/account.rb +3 -0
@@ 102,6 102,7 @@ class Account < ApplicationRecord
  scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
  scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) }
  scope :popular, -> { order('account_stats.followers_count desc') }
  scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }

  delegate :email,
           :unconfirmed_email,


@@ 110,6 111,8 @@ class Account < ApplicationRecord
           :confirmed?,
           :approved?,
           :pending?,
           :disabled?,
           :role,
           :admin?,
           :moderator?,
           :staff?,

M app/models/account_filter.rb => app/models/account_filter.rb +2 -0
@@ 37,6 37,8 @@ class AccountFilter
      Account.without_suspended
    when 'pending'
      accounts_with_users.merge User.pending
    when 'disabled'
      accounts_with_users.merge User.disabled
    when 'silenced'
      Account.silenced
    when 'suspended'

M app/models/concerns/attachmentable.rb => app/models/concerns/attachmentable.rb +18 -1
@@ 1,6 1,6 @@
# frozen_string_literal: true

require 'mime/types'
require 'mime/types/columnar'

module Attachmentable
  extend ActiveSupport::Concern


@@ 10,10 10,21 @@ module Attachmentable
  included do
    before_post_process :set_file_extensions
    before_post_process :check_image_dimensions
    before_post_process :set_file_content_type
  end

  private

  def set_file_content_type
    self.class.attachment_definitions.each_key do |attachment_name|
      attachment = send(attachment_name)

      next if attachment.blank? || attachment.queued_for_write[:original].blank?

      attachment.instance_write :content_type, calculated_content_type(attachment)
    end
  end

  def set_file_extensions
    self.class.attachment_definitions.each_key do |attachment_name|
      attachment = send(attachment_name)


@@ 47,4 58,10 @@ module Attachmentable

    extension
  end

  def calculated_content_type(attachment)
    Paperclip.run('file', '-b --mime :file', file: attachment.queued_for_write[:original].path).split(/[:;\s]+/).first.chomp
  rescue Terrapin::CommandLineError
    ''
  end
end

M app/models/concerns/user_roles.rb => app/models/concerns/user_roles.rb +14 -0
@@ 13,6 13,20 @@ module UserRoles
    admin? || moderator?
  end

  def role=(value)
    case value
    when 'admin'
      self.admin     = true
      self.moderator = false
    when 'moderator'
      self.admin     = false
      self.moderator = true
    else
      self.admin     = false
      self.moderator = false
    end
  end

  def role
    if admin?
      'admin'

M app/models/custom_emoji.rb => app/models/custom_emoji.rb +1 -0
@@ 39,6 39,7 @@ class CustomEmoji < ApplicationRecord
  scope :local,      -> { where(domain: nil) }
  scope :remote,     -> { where.not(domain: nil) }
  scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) }
  scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }

  remotable_attachment :image, LIMIT


M app/models/domain_block.rb => app/models/domain_block.rb +30 -3
@@ 24,14 24,41 @@ class DomainBlock < ApplicationRecord

  scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }

  def self.blocked?(domain)
    where(domain: domain, severity: :suspend).exists?
  class << self
    def suspend?(domain)
      !!rule_for(domain)&.suspend?
    end

    def silence?(domain)
      !!rule_for(domain)&.silence?
    end

    def reject_media?(domain)
      !!rule_for(domain)&.reject_media?
    end

    def reject_reports?(domain)
      !!rule_for(domain)&.reject_reports?
    end

    alias blocked? suspend?

    def rule_for(domain)
      return if domain.blank?

      uri      = Addressable::URI.new.tap { |u| u.host = domain.gsub(/[\/]/, '') }
      segments = uri.normalized_host.split('.')
      variants = segments.map.with_index { |_, i| segments[i..-1].join('.') }

      where(domain: variants[0..-2]).order(Arel.sql('char_length(domain) desc')).first
    end
  end

  def stricter_than?(other_block)
    return true if suspend?
    return true  if suspend?
    return false if other_block.suspend? && (silence? || noop?)
    return false if other_block.silence? && noop?

    (reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports)
  end


M app/models/instance.rb => app/models/instance.rb +3 -7
@@ 8,15 8,11 @@ class Instance
  def initialize(resource)
    @domain         = resource.domain
    @accounts_count = resource.is_a?(DomainBlock) ? nil : resource.accounts_count
    @domain_block   = resource.is_a?(DomainBlock) ? resource : DomainBlock.find_by(domain: domain)
    @domain_block   = resource.is_a?(DomainBlock) ? resource : DomainBlock.rule_for(domain)
  end

  def cached_sample_accounts
    Rails.cache.fetch("#{cache_key}/sample_accounts", expires_in: 12.hours) { Account.where(domain: domain).searchable.joins(:account_stat).popular.limit(3) }
  end

  def cached_accounts_count
    @accounts_count || Rails.cache.fetch("#{cache_key}/count", expires_in: 12.hours) { Account.where(domain: domain).count }
  def countable?
    @accounts_count.present?
  end

  def to_param

M app/models/media_attachment.rb => app/models/media_attachment.rb +59 -45
@@ 24,16 24,16 @@
class MediaAttachment < ApplicationRecord
  self.inheritance_column = nil

  enum type: [:image, :gifv, :video, :audio, :unknown]
  enum type: [:image, :gifv, :video, :unknown, :audio]

  IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].freeze
  VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v', '.mov'].freeze
  AUDIO_FILE_EXTENSIONS = ['.mp3', '.m4a', '.wav', '.ogg'].freeze
  AUDIO_FILE_EXTENSIONS = ['.ogg', '.oga', '.mp3', '.m4a', '.wav', '.flac', '.opus'].freeze

  IMAGE_MIME_TYPES             = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
  VIDEO_MIME_TYPES             = ['video/webm', 'video/mp4', 'video/quicktime'].freeze
  VIDEO_MIME_TYPES             = ['video/webm', 'video/mp4', 'video/quicktime', 'video/ogg'].freeze
  VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze
  AUDIO_MIME_TYPES             = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze
  AUDIO_MIME_TYPES             = ['audio/wave', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/vdn.wav', 'audio/x-pn-wave', 'audio/ogg', 'audio/mpeg', 'audio/mp3', 'audio/mp4', 'audio/webm', 'audio/flac'].freeze

  BLURHASH_OPTIONS = {
    x_comp: 4,


@@ 53,22 53,6 @@ class MediaAttachment < ApplicationRecord
    },
  }.freeze

  AUDIO_STYLES = {
    original: {
      format: 'mp4',
      convert_options: {
        output: {
          filter_complex: '"[0:a]compand,showwaves=s=640x360:mode=line,format=yuv420p[v]"',
          map: '"[v]" -map 0:a', 
          threads: 2,
          vcodec: 'libx264',
          acodec: 'aac',
          movflags: '+faststart',
        },
      },
    },
  }.freeze

  VIDEO_STYLES = {
    small: {
      convert_options: {


@@ 83,8 67,21 @@ class MediaAttachment < ApplicationRecord
    },
  }.freeze

  AUDIO_STYLES = {
    original: {
      format: 'mp3',
      content_type: 'audio/mpeg',
      convert_options: {
        output: {
          'q:a' => 2,
        },
      },
    },
  }.freeze

  VIDEO_FORMAT = {
    format: 'mp4',
    content_type: 'video/mp4',
    convert_options: {
      output: {
        'loglevel' => 'fatal',


@@ 101,6 98,11 @@ class MediaAttachment < ApplicationRecord
    },
  }.freeze

  VIDEO_CONVERTED_STYLES = {
    small: VIDEO_STYLES[:small],
    original: VIDEO_FORMAT,
  }.freeze

  IMAGE_LIMIT = (ENV['MAX_IMAGE_SIZE'] || 8.megabytes).to_i
  VIDEO_LIMIT = (ENV['MAX_VIDEO_SIZE'] || 40.megabytes).to_i



@@ 114,8 116,8 @@ class MediaAttachment < ApplicationRecord
                    convert_options: { all: '-quality 90 -strip' }

  validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
  validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video_or_gifv?
  validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video_or_gifv?
  validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :larger_media_format?
  validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :larger_media_format?
  remotable_attachment :file, VIDEO_LIMIT

  include Attachmentable


@@ 138,8 140,12 @@ class MediaAttachment < ApplicationRecord
    file.blank? && remote_url.present?
  end

  def video_or_gifv?
    video? || gifv?
  def larger_media_format?
    video? || gifv? || audio?
  end

  def audio_or_video?
    audio? || video?
  end

  def to_param


@@ 171,37 177,37 @@ class MediaAttachment < ApplicationRecord
  before_save :set_meta

  class << self
    def supported_mime_types
      IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
    end

    def supported_file_extensions
      IMAGE_FILE_EXTENSIONS + VIDEO_FILE_EXTENSIONS + AUDIO_FILE_EXTENSIONS
    end

    private

    def file_styles(f)
      if f.instance.file_content_type == 'image/gif'
        {
          small: IMAGE_STYLES[:small],
          original: VIDEO_FORMAT,
        }
      elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type
      if f.instance.file_content_type == 'image/gif' || VIDEO_CONVERTIBLE_MIME_TYPES.include?(f.instance.file_content_type)
        VIDEO_CONVERTED_STYLES
      elsif IMAGE_MIME_TYPES.include?(f.instance.file_content_type)
        IMAGE_STYLES
      elsif AUDIO_MIME_TYPES.include? f.instance.file_content_type
        AUDIO_STYLES
      elsif VIDEO_CONVERTIBLE_MIME_TYPES.include?(f.instance.file_content_type)
        {
          small: VIDEO_STYLES[:small],
          original: VIDEO_FORMAT,
        }
      else
      elsif VIDEO_MIME_TYPES.include?(f.instance.file_content_type)
        VIDEO_STYLES
      else
        AUDIO_STYLES
      end
    end

    def file_processors(f)
      if f.file_content_type == 'image/gif'
        [:gif_transcoder, :blurhash_transcoder]
      elsif VIDEO_MIME_TYPES.include? f.file_content_type
        [:video_transcoder, :blurhash_transcoder]
      elsif AUDIO_MIME_TYPES.include? f.file_content_type
        [:audio_transcoder]
      elsif VIDEO_MIME_TYPES.include?(f.file_content_type)
        [:video_transcoder, :blurhash_transcoder, :type_corrector]
      elsif AUDIO_MIME_TYPES.include?(f.file_content_type)
        [:transcoder, :type_corrector]
      else
        [:lazy_thumbnail, :blurhash_transcoder]
        [:lazy_thumbnail, :blurhash_transcoder, :type_corrector]
      end
    end
  end


@@ 224,7 230,15 @@ class MediaAttachment < ApplicationRecord
  end

  def set_type_and_extension
    self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : AUDIO_MIME_TYPES.include?(file_content_type) ? :audio : :image
    self.type = begin
      if VIDEO_MIME_TYPES.include?(file_content_type)
        :video
      elsif AUDIO_MIME_TYPES.include?(file_content_type)
        :audio
      else
        :image
      end
    end
  end

  def set_meta


@@ 267,7 281,7 @@ class MediaAttachment < ApplicationRecord
      frame_rate: movie.frame_rate,
      duration: movie.duration,
      bitrate: movie.bitrate,
    }
    }.compact
  end

  def reset_parent_cache

M app/models/report.rb => app/models/report.rb +3 -0
@@ 17,6 17,8 @@
#

class Report < ApplicationRecord
  include Paginable

  belongs_to :account
  belongs_to :target_account, class_name: 'Account'
  belongs_to :action_taken_by_account, class_name: 'Account', optional: true


@@ 26,6 28,7 @@ class Report < ApplicationRecord

  scope :unresolved, -> { where(action_taken: false) }
  scope :resolved,   -> { where(action_taken: true) }
  scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].each_with_object({}) { |k, h| h[k] = { user: [:invite_request, :invite] } }) }

  validates :comment, length: { maximum: 1000 }


M app/models/report_filter.rb => app/models/report_filter.rb +2 -0
@@ 9,9 9,11 @@ class ReportFilter

  def results
    scope = Report.unresolved

    params.each do |key, value|
      scope = scope.merge scope_for(key, value)
    end

    scope
  end


M app/models/user.rb => app/models/user.rb +1 -0
@@ 87,6 87,7 @@ class User < ApplicationRecord
  scope :approved, -> { where(approved: true) }
  scope :confirmed, -> { where.not(confirmed_at: nil) }
  scope :enabled, -> { where(disabled: false) }
  scope :disabled, -> { where(disabled: true) }
  scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
  scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) }
  scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }

M app/serializers/initial_state_serializer.rb => app/serializers/initial_state_serializer.rb +1 -1
@@ 76,7 76,7 @@ class InitialStateSerializer < ActiveModel::Serializer
  end

  def media_attachments
    { accept_content_types: MediaAttachment::IMAGE_FILE_EXTENSIONS + MediaAttachment::VIDEO_FILE_EXTENSIONS + MediaAttachment::AUDIO_FILE_EXTENSIONS + MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES + MediaAttachment::AUDIO_MIME_TYPES }
    { accept_content_types: MediaAttachment.supported_file_extensions + MediaAttachment.supported_mime_types }
  end

  private

A app/serializers/rest/admin/account_serializer.rb => app/serializers/rest/admin/account_serializer.rb +77 -0
@@ 0,0 1,77 @@
# frozen_string_literal: true

class REST::Admin::AccountSerializer < ActiveModel::Serializer
  attributes :id, :username, :domain, :created_at,
             :email, :ip, :role, :confirmed, :suspended,
             :silenced, :disabled, :approved, :locale,
             :invite_request

  attribute :created_by_application_id, if: :created_by_application?
  attribute :invited_by_account_id, if: :invited?

  has_one :account, serializer: REST::AccountSerializer

  def id
    object.id.to_s
  end

  def email
    object.user_email
  end

  def ip
    object.user_current_sign_in_ip.to_s.presence
  end

  def role
    object.user_role
  end

  def suspended
    object.suspended?
  end

  def silenced
    object.silenced?
  end

  def confirmed
    object.user_confirmed?
  end

  def disabled
    object.user_disabled?
  end

  def approved
    object.user_approved?
  end

  def account
    object
  end

  def locale
    object.user_locale
  end

  def created_by_application_id
    object.user&.created_by_application_id&.to_s&.presence
  end

  def invite_request
    object.user&.invite_request&.text
  end

  def invited_by_account_id
    object.user&.invite&.user&.account_id&.to_s&.presence
  end

  def invited?
    object.user&.invited?
  end

  def created_by_application?
    object.user&.created_by_application_id&.present?
  end
end

A app/serializers/rest/admin/report_serializer.rb => app/serializers/rest/admin/report_serializer.rb +16 -0
@@ 0,0 1,16 @@
# frozen_string_literal: true

class REST::Admin::ReportSerializer < ActiveModel::Serializer
  attributes :id, :action_taken, :comment, :created_at, :updated_at

  has_one :account, serializer: REST::Admin::AccountSerializer
  has_one :target_account, serializer: REST::Admin::AccountSerializer
  has_one :assigned_account, serializer: REST::Admin::AccountSerializer
  has_one :action_taken_by_account, serializer: REST::Admin::AccountSerializer

  has_many :statuses, serializer: REST::StatusSerializer

  def id
    object.id.to_s
  end
end

M app/serializers/rest/instance_serializer.rb => app/serializers/rest/instance_serializer.rb +10 -2
@@ 3,9 3,9 @@
class REST::InstanceSerializer < ActiveModel::Serializer
  include RoutingHelper

  attributes :uri, :title, :description, :email,
  attributes :uri, :title, :short_description, :description, :email,
             :version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits,
             :languages, :registrations
             :languages, :registrations, :approval_required

  has_one :contact_account, serializer: REST::AccountSerializer



@@ 19,6 19,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
    Setting.site_title
  end

  def short_description
    Setting.site_short_description
  end

  def description
    Setting.site_description
  end


@@ 68,6 72,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
    Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode
  end

  def approval_required
    Setting.registrations_mode == 'approved'
  end

  private

  def instance_presenter

M app/services/activitypub/process_account_service.rb => app/services/activitypub/process_account_service.rb +1 -1
@@ 205,7 205,7 @@ class ActivityPub::ProcessAccountService < BaseService

  def domain_block
    return @domain_block if defined?(@domain_block)
    @domain_block = DomainBlock.find_by(domain: @domain)
    @domain_block = DomainBlock.rule_for(@domain)
  end

  def key_changed?

M app/services/block_domain_service.rb => app/services/block_domain_service.rb +2 -2
@@ 76,7 76,7 @@ class BlockDomainService < BaseService
  end

  def blocked_domain_accounts
    Account.where(domain: blocked_domain)
    Account.by_domain_and_subdomains(blocked_domain)
  end

  def media_from_blocked_domain


@@ 84,6 84,6 @@ class BlockDomainService < BaseService
  end

  def emojis_from_blocked_domains
    CustomEmoji.where(domain: blocked_domain)
    CustomEmoji.by_domain_and_subdomains(blocked_domain)
  end
end

M app/services/post_status_service.rb => app/services/post_status_service.rb +1 -1
@@ 107,7 107,7 @@ class PostStatusService < BaseService

    @media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i))

    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:video?)
    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:audio_or_video?)
  end

  def language_from_option(str)

M app/services/resolve_account_service.rb => app/services/resolve_account_service.rb +1 -1
@@ 146,7 146,7 @@ class ResolveAccountService < BaseService

  def domain_block
    return @domain_block if defined?(@domain_block)
    @domain_block = DomainBlock.find_by(domain: @domain)
    @domain_block = DomainBlock.rule_for(@domain)
  end

  def atom_url

M app/services/unblock_domain_service.rb => app/services/unblock_domain_service.rb +2 -1
@@ 14,7 14,8 @@ class UnblockDomainService < BaseService
  end

  def blocked_accounts
    scope = Account.where(domain: domain_block.domain)
    scope = Account.by_domain_and_subdomains(domain_block.domain)

    if domain_block.silence?
      scope.where(silenced_at: @domain_block.created_at)
    else

M app/services/update_remote_profile_service.rb => app/services/update_remote_profile_service.rb +2 -2
@@ 26,7 26,7 @@ class UpdateRemoteProfileService < BaseService
    account.note         = remote_profile.note         || ''
    account.locked       = remote_profile.locked?

    if !account.suspended? && !DomainBlock.find_by(domain: account.domain)&.reject_media?
    if !account.suspended? && !DomainBlock.reject_media?(account.domain)
      if remote_profile.avatar.present?
        account.avatar_remote_url = remote_profile.avatar
      else


@@ 46,7 46,7 @@ class UpdateRemoteProfileService < BaseService
  end

  def save_emojis
    do_not_download = DomainBlock.find_by(domain: account.domain)&.reject_media?
    do_not_download = DomainBlock.reject_media?(account.domain)

    return if do_not_download


M app/views/admin/instances/index.html.haml => app/views/admin/instances/index.html.haml +11 -10
@@ 33,21 33,22 @@
      %h4
        = instance.domain
        %small
          = t('admin.instances.known_accounts', count: instance.cached_accounts_count)

          - if instance.domain_block
            - first_item = true
            - if !instance.domain_block.noop?
              &bull;
              = t("admin.domain_blocks.severity.#{instance.domain_block.severity}")
              - first_item = false
            - if instance.domain_block.reject_media?
              &bull;
              - unless first_item
                &bull;
              = t('admin.domain_blocks.rejecting_media')
              - first_item = false
            - if instance.domain_block.reject_reports?
              &bull;
              - unless first_item
                &bull;
              = t('admin.domain_blocks.rejecting_reports')

      .avatar-stack
        - instance.cached_sample_accounts.each do |account|
          = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar'

          - else
            = t('admin.accounts.no_limits_imposed')
      - if instance.countable?
        .trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true
= paginate paginated_instances

M app/views/stream_entries/_detailed_status.html.haml => app/views/stream_entries/_detailed_status.html.haml +1 -1
@@ 27,7 27,7 @@
          = render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }

  - if !status.media_attachments.empty?
    - if status.media_attachments.first.video?
    - if status.media_attachments.first.audio_or_video?
      - video = status.media_attachments.first
      = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
        = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments }

M app/views/stream_entries/_simple_status.html.haml => app/views/stream_entries/_simple_status.html.haml +1 -1
@@ 31,7 31,7 @@
          = render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }

  - if !status.media_attachments.empty?
    - if status.media_attachments.first.video?
    - if status.media_attachments.first.audio_or_video?
      - video = status.media_attachments.first
      = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do
        = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments }

M config/application.rb => config/application.rb +1 -1
@@ 10,7 10,7 @@ require_relative '../app/lib/exceptions'
require_relative '../lib/paperclip/lazy_thumbnail'
require_relative '../lib/paperclip/gif_transcoder'
require_relative '../lib/paperclip/video_transcoder'
require_relative '../lib/paperclip/audio_transcoder'
require_relative '../lib/paperclip/type_corrector'
require_relative '../lib/mastodon/snowflake'
require_relative '../lib/mastodon/version'
require_relative '../lib/devise/ldap_authenticatable'

M config/initializers/doorkeeper.rb => config/initializers/doorkeeper.rb +7 -1
@@ 82,7 82,13 @@ Doorkeeper.configure do
                  :'read:search',
                  :'read:statuses',
                  :follow,
                  :push
                  :push,
                  :'admin:read',
                  :'admin:read:accounts',
                  :'admin:read:reports',
                  :'admin:write',
                  :'admin:write:accounts',
                  :'admin:write:reports'

  # Change the way client credentials are retrieved from the request object.
  # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then

M config/locales/activerecord.fi.yml => config/locales/activerecord.fi.yml +12 -0
@@ 1,1 1,13 @@
---
fi:
  activerecord:
    attributes:
      poll:
        expires_at: Määräaika
        options: Vaihtoehdot
    errors:
      models:
        account:
          attributes:
            username:
              invalid: Vain kirjaimia, numeroita ja alleviivoja

M config/locales/activerecord.it.yml => config/locales/activerecord.it.yml +5 -1
@@ 1,12 1,16 @@
---
it:
  activerecord:
    attributes:
      poll:
        expires_at: Scadenza
        options: Scelte
    errors:
      models:
        account:
          attributes:
            username:
              invalid: solo lettere, numeri e trattino basso
              invalid: solo lettere, numeri e trattini bassi
        status:
          attributes:
            reblog:

M config/locales/devise.it.yml => config/locales/devise.it.yml +3 -0
@@ 12,6 12,7 @@ it:
      last_attempt: Hai un altro tentativo prima che il tuo account venga bloccato.
      locked: Il tuo account è stato bloccato.
      not_found_in_database: "%{authentication_keys} o password invalida."
      pending: Il tuo account è ancora in fase di approvazione.
      timeout: La tua sessione è terminata. Per favore, effettua l'accesso o registrati per continuare.
      unauthenticated: Devi effettuare l'accesso o registrarti per continuare.
      unconfirmed: Devi confermare il tuo indirizzo email per continuare.


@@ 20,6 21,7 @@ it:
        action: Verifica indirizzo email
        action_with_app: Conferma e torna a %{app}
        explanation: Hai creato un account su %{host} con questo indirizzo email. Sei lonatno solo un clic dall'attivarlo. Se non sei stato tu, per favore ignora questa email.
        explanation_when_pending: Hai richiesto un invito a %{host} con questo indirizzo email. Una volta confermato il tuo indirizzo e-mail, analizzeremo la tua richiesta. Non potrai eseguire l'accesso fino a quel momento. Se la tua richiesta sarà rifiutata, i tuoi dati saranno rimossi, quindi nessun'altra azione ti sarà richiesta. Se non fossi stato tu, per favore ignora questa email.
        extra_html: Per favore controlla<a href="%{terms_path}">le regole del server</a> e <a href="%{policy_path}">i nostri termini di servizio</a>.
        subject: 'Mastodon: Istruzioni di conferma per %{instance}'
        title: Verifica indirizzo email


@@ 60,6 62,7 @@ it:
      signed_up: Benvenuto! Ti sei registrato con successo.
      signed_up_but_inactive: Ti sei registrato con successo. Purtroppo però non possiamo farti accedere perché non hai ancora attivato il tuo account.
      signed_up_but_locked: Ti sei registrato con successo. Purtroppo però non possiamo farti accedere perché il tuo account è bloccato.
      signed_up_but_pending: Un messaggio con un collegamento per la conferma è stato inviato al tuo indirizzo email. Dopo aver cliccato il collegamento, esamineremo la tua richiesta. Ti sarà notificato se verrà approvata.
      signed_up_but_unconfirmed: Un messaggio con un link di conferma è stato inviato al tuo indirizzo email. Per favore, visita il link per attivare il tuo account.
      update_needs_confirmation: Hai aggiornato correttamente il tuo account, ma abbiamo bisogno di verificare il tuo nuovo indirizzo email. Per favore, controlla la posta in arrivo e visita il link di conferma per verificare il tuo indirizzo email.
      updated: Il tuo account è stato aggiornato con successo.

M config/locales/doorkeeper.ca.yml => config/locales/doorkeeper.ca.yml +6 -0
@@ 114,6 114,12 @@ ca:
      application:
        title: OAuth autorització requerida
    scopes:
      admin:read: llegir totes les dades en el servidor
      admin:read:accounts: llegir l'informació sensible de tots els comptes
      admin:read:reports: llegir l'informació sensible de tots els informes i comptes reportats
      admin:write: modificar totes les dades en el servidor
      admin:write:accounts: fer l'acció de moderació en els comptes
      admin:write:reports: fer l'acció de moderació en els informes
      follow: seguir, blocar, desblocar i deixar de seguir comptes
      push: rebre notificacions push del teu compte
      read: llegir les dades del teu compte

M config/locales/doorkeeper.co.yml => config/locales/doorkeeper.co.yml +6 -0
@@ 114,6 114,12 @@ co:
      application:
        title: Auturizazione OAuth riquestata
    scopes:
      admin:read: leghje tutti i dati nant'à u servore
      admin:read:accounts: leghje i cuntinuti sensibili di tutti i conti
      admin:read:reports: leghje i cuntinuti sensibili di tutti i rapporti è conti signalati
      admin:write: mudificà tutti i dati nant'à u servore
      admin:write:accounts: realizà azzione di muderazione nant'à i conti
      admin:write:reports: realizà azzione di muderazione nant'à i rapporti
      follow: Mudificà rilazione trà i conti
      push: Riceve e vostre nutificazione push
      read: leghje tutte l’infurmazioni di u vostru contu

M config/locales/doorkeeper.cs.yml => config/locales/doorkeeper.cs.yml +6 -0
@@ 114,6 114,12 @@ cs:
      application:
        title: Je požadována autorizace OAuth
    scopes:
      admin:read: číst všechna data na serveru
      admin:read:accounts: číst citlivé informace všech účtů
      admin:read:reports: číst citlivé informace všech nahlášení a nahlášených účtů
      admin:write: měnit všechna data na serveru
      admin:write:accounts: provádět moderátorské akce s účty
      admin:write:reports: provádět moderátorské akce s nahlášeními
      follow: upravovat vztahy mezi profily
      push: přijímat vaše push oznámení
      read: vidět všechna data vašeho účtu

M config/locales/doorkeeper.de.yml => config/locales/doorkeeper.de.yml +6 -0
@@ 114,6 114,12 @@ de:
      application:
        title: OAuth-Autorisierung nötig
    scopes:
      admin:read: alle Daten auf dem Server lesen
      admin:read:accounts: sensible Daten aller Konten lesen
      admin:read:reports: sensible Daten aller Meldungen und gemeldeten Konten lesen
      admin:write: alle Daten auf dem Server ändern
      admin:write:accounts: Moderationsaktionen auf Konten ausführen
      admin:write:reports: Moderationsaktionen auf Meldungen ausführen
      follow: Kontenbeziehungen verändern
      push: deine Push-Benachrichtigungen erhalten
      read: all deine Daten lesen

M config/locales/doorkeeper.el.yml => config/locales/doorkeeper.el.yml +6 -0
@@ 114,6 114,12 @@ el:
      application:
        title: Απαιτείται έγκριση OAuth
    scopes:
      admin:read: ανάγνωση δεδομένων στον διακομιστή
      admin:read:accounts: ανάγνωση ευαίσθητων πληροφοριών όλων των λογαριασμών
      admin:read:reports: ανάγνωση ευαίσθητων πληροφοριών όλων των καταγγελιών και των καταγγελλομένων λογαριασμών
      admin:write: αλλαγή δεδομένων στον διακομιστή
      admin:write:accounts: εκτέλεση διαχειριστικών ενεργειών σε λογαριασμούς
      admin:write:reports: εκτέλεση διαχειριστικών ενεργειών σε καταγγελίες
      follow: να αλλάζει τις σχέσεις με λογαριασμούς
      push: να λαμβάνει τις ειδοποιήσεις σου
      read: να διαβάζει όλα τα στοιχεία του λογαριασμού σου

M config/locales/doorkeeper.en.yml => config/locales/doorkeeper.en.yml +6 -0
@@ 114,6 114,12 @@ en:
      application:
        title: OAuth authorization required
    scopes:
      admin:read: read all data on the server
      admin:read:accounts: read sensitive information of all accounts
      admin:read:reports: read sensitive information of all reports and reported accounts
      admin:write: modify all data on the server
      admin:write:accounts: perform moderation actions on accounts
      admin:write:reports: perform moderation actions on reports
      follow: modify account relationships
      push: receive your push notifications
      read: read all your account's data

M config/locales/doorkeeper.gl.yml => config/locales/doorkeeper.gl.yml +6 -0
@@ 114,6 114,12 @@ gl:
      application:
        title: Precisa autorización OAuth
    scopes:
      admin:read: ler todos os datos no servidor
      admin:read:accounts: ler información sensible de todas as contas
      admin:read:reports: ler información sensible de todos os informes e contas reportadas
      admin:write: modificar todos os datos no servidor
      admin:write:accounts: executar accións de moderación nas contas
      admin:write:reports: executar accións de moderación nos informes
      follow: modificar as relacións da conta
      push: recibir notificacións push
      read: ler todos os datos da súa conta

M config/locales/doorkeeper.it.yml => config/locales/doorkeeper.it.yml +8 -0
@@ 36,9 36,11 @@ it:
        scopes: Dividi gli scopes con spazi. Lascia vuoto per utilizzare gli scopes di default.
      index:
        application: Applicazione
        callback_url: URL di callback
        delete: Elimina
        name: Nome
        new: Nuova applicazione
        scopes: Visibilità
        show: Mostra
        title: Le tue applicazioni
      new:


@@ 112,6 114,12 @@ it:
      application:
        title: Autorizzazione OAuth richiesta
    scopes:
      admin:read: leggere tutti i dati dal server
      admin:read:accounts: leggere dati sensibili di tutti gli account
      admin:read:reports: leggere dati sensibili di tutte le segnalazioni e gli account segnalati
      admin:write: modificare tutti i dati sul server
      admin:write:accounts: eseguire azioni di moderazione sugli account
      admin:write:reports: eseguire azioni di moderazione sulle segnalazioni
      follow: modificare relazioni tra account
      push: ricevere le tue notifiche push
      read: leggere tutte le informazioni del tuo account

M config/locales/doorkeeper.ja.yml => config/locales/doorkeeper.ja.yml +6 -0
@@ 114,6 114,12 @@ ja:
      application:
        title: OAuth認証
    scopes:
      admin:read: サーバーのすべてのデータの読み取り
      admin:read:accounts: すべてのアカウントの機密情報の読み取り
      admin:read:reports: すべての通報と通報されたアカウントの機密情報の読み取り
      admin:write: サーバーのすべてのデータの変更
      admin:write:accounts: アカウントに対するアクションの実行
      admin:write:reports: 通報に対するアクションの実行
      follow: アカウントのつながりを変更
      push: プッシュ通知の受信
      read: アカウントのすべてのデータの読み取り

M config/locales/doorkeeper.pl.yml => config/locales/doorkeeper.pl.yml +6 -0
@@ 114,6 114,12 @@ pl:
      application:
        title: Uwierzytelnienie OAuth jest wymagane
    scopes:
      admin:read: odczytaj wszystkie dane na serwerze
      admin:read:accounts: odczytaj wrażliwe informacje na wszystkich kontach
      admin:read:reports: odczytaj wrażliwe informacje ze wszystkich zgłoszeń oraz zgłoszonych kont
      admin:write: zmodyfikuj wszystkie dane na serwerze
      admin:write:accounts: wykonaj działania moderacyjne na kontach
      admin:write:reports: wykonaj działania moderacyjne na zgłoszeniach
      follow: możliwość śledzenia kont
      push: otrzymywanie powiadomień push dla Twojego konta
      read: możliwość odczytu wszystkich danych konta

M config/locales/doorkeeper.sl.yml => config/locales/doorkeeper.sl.yml +6 -0
@@ 114,6 114,12 @@ sl:
      application:
        title: Potrebna je OAuth pooblastitev
    scopes:
      admin:read: preberi vse podatke na strežniku
      admin:read:accounts: preberi občutljive informacije vseh računov
      admin:read:reports: preberi občutljive informacije vseh prijav in prijavljenih računov
      admin:write: spremeni vse podatke na strežniku
      admin:write:accounts: izvedi moderirana dejanja na računih
      admin:write:reports: izvedi moderirana dejanja na prijavah
      follow: spremeni razmerja med računi
      push: prejmi potisna obvestila
      read: preberi vse podatke svojega računa

M config/locales/doorkeeper.zh-CN.yml => config/locales/doorkeeper.zh-CN.yml +6 -0
@@ 113,6 113,12 @@ zh-CN:
      application:
        title: 需要 OAuth 认证
    scopes:
      admin:read: 读取服务器上的所有数据
      admin:read:accounts: 读取所有账户的敏感信息
      admin:read:reports: 读取所有举报和被举报账户的敏感信息
      admin:write: 修改服务器上的所有数据
      admin:write:accounts: 对账户执行管理操作
      admin:write:reports: 对举报执行管理操作
      follow: 关注或屏蔽用户
      push: 接收你的帐户的推送通知
      read: 读取你的帐户数据

M config/locales/it.yml => config/locales/it.yml +212 -4
@@ 7,23 7,33 @@ it:
    active_count_after: attivo
    active_footnote: Utenti Attivi Mensili (MAU)
    administered_by: 'Amministrato da:'
    api: API
    apps: Applicazioni Mobile
    apps_platforms: Usa Mastodon da iOS, Android e altre piattaforme
    browse_directory: Sfoglia la directory dei profili e filtra per interessi
    browse_public_posts: Sfoglia il flusso in tempo reale di post pubblici su Mastodon
    contact: Contatti
    contact_missing: Non impostato
    contact_unavailable: N/D
    discover_users: Scopri utenti
    documentation: Documentazione
    extended_description_html: |
      <h3>Un buon posto per le regole</h3>
      <p>La descrizione estesa non è ancora stata preparata.</p>
    federation_hint_html: Con un account su %{instance} sarai in grado di seguire persone su qualsiasi server Mastodon e oltre.
    generic_description: "%{domain} è un server nella rete"
    get_apps: Prova l'app per smartphone
    get_apps: Prova un'app per smartphone
    hosted_on: Mastodon ospitato su %{domain}
    learn_more: Scopri altro
    privacy_policy: Politica della privacy
    see_whats_happening: Guarda cosa succede
    server_stats: 'Statistiche del server:'
    source_code: Codice sorgente
    status_count_after:
      one: stato
      other: stati
    status_count_before: Che hanno pubblicato
    tagline: Segui vecchi amici e trovane nuovi
    tagline: Segui amici e trovane di nuovi
    terms: Termini di Servizio
    user_count_after:
      one: utente


@@ 40,6 50,7 @@ it:
    joined: Dal %{date}
    last_active: ultima attività
    link_verified_on: La proprietà di questo link è stata controllata il %{date}
    media: Media
    moved_html: "%{name} è stato spostato su %{new_profile_link}:"
    network_hidden: Questa informazione non e' disponibile
    nothing_here: Qui non c'è nulla!


@@ 47,12 58,17 @@ it:
    people_who_follow: Persone che seguono %{name}
    pin_errors:
      following: Devi gia seguire la persona che vuoi promuovere
    posts:
      one: Toot
      other: Toot
    posts_tab_heading: Toot
    posts_with_replies: Toot e risposte
    reserved_username: Il nome utente è gia stato preso
    roles:
      admin: Amministratore
      bot: Bot
      moderator: Moderatore
    unavailable: Profilo non disponibile
    unfollow: Non seguire più
  admin:
    account_actions:


@@ 64,7 80,10 @@ it:
      delete: Elimina
      destroyed_msg: Nota di moderazione distrutta con successo!
    accounts:
      approve: Approva
      approve_all: Approva tutto
      are_you_sure: Sei sicuro?
      avatar: Immagine di profilo
      by_domain: Dominio
      change_email:
        changed_msg: Account email cambiato con successo!


@@ 84,6 103,7 @@ it:
      display_name: Nome visualizzato
      domain: Dominio
      edit: Modifica
      email: Email
      email_status: Stato email
      enable: Abilita
      enabled: Abilitato


@@ 94,6 114,7 @@ it:
      header: Intestazione
      inbox_url: URL inbox
      invited_by: Invitato da
      ip: IP
      joined: Unito
      location:
        all: Tutto


@@ 106,15 127,18 @@ it:
      moderation:
        active: Attivo
        all: Tutto
        pending: In attesa
        silenced: Silenziati
        suspended: Sospesi
        title: Moderazione
      moderation_notes: Note di moderazione
      most_recent_activity: Attività più recenti
      most_recent_ip: IP più recenti
      no_account_selected: Nessun account è stato modificato visto che non ne è stato selezionato nessuno
      no_limits_imposed: Nessun limite imposto
      not_subscribed: Non sottoscritto
      outbox_url: URL outbox
      pending: Revisioni in attesa
      perform_full_suspension: Sospendi
      profile_url: URL profilo
      promote: Promuovi


@@ 122,6 146,8 @@ it:
      public: Pubblico
      push_subscription_expires: Sottoscrizione PuSH scaduta
      redownload: Aggiorna avatar
      reject: Rifiuta
      reject_all: Rifiuta tutto
      remove_avatar: Rimuovi avatar
      remove_header: Rimuovi intestazione
      resend_confirmation:


@@ 148,6 174,7 @@ it:
      statuses: Stati
      subscribe: Sottoscrivi
      suspended: Sospeso
      time_in_queue: Attesa in coda %{time}
      title: Account
      unconfirmed_email: Email non confermata
      undo_silenced: Rimuovi silenzia


@@ 155,6 182,7 @@ it:
      unsubscribe: Annulla l'iscrizione
      username: Nome utente
      warn: Avverti
      web: Web
    action_logs:
      actions:
        assigned_to_self_report: "%{name} ha assegnato il rapporto %{target} a se stesso"


@@ 188,6 216,7 @@ it:
        update_custom_emoji: "%{name} ha aggiornato l'emoji %{target}"
        update_status: "%{name} stato aggiornato da %{target}"
      deleted_status: "(stato cancellato)"
      title: Registro di controllo
    custom_emojis:
      by_domain: Dominio
      copied_msg: Creata con successo una copia locale dell'emoji


@@ 198,6 227,7 @@ it:
      destroyed_msg: Emoji distrutto con successo!
      disable: Disabilita
      disabled_msg: Questa emoji è stata disabilitata con successo
      emoji: Emoji
      enable: Abilita
      enabled_msg: Questa emoji è stata abilitata con successo
      image_hint: PNG fino a 50 KB


@@ 205,6 235,7 @@ it:
      new:
        title: Aggiungi nuovo emoji personalizzato
      overwrite: Sovrascrivi
      shortcode: Scorciatoia
      shortcode_hint: Almeno due caratteri, solo caratteri alfanumerici e trattino basso
      title: Emoji personalizzate
      unlisted: Non elencato


@@ 212,19 243,23 @@ it:
      updated_msg: Emoji aggiornata con successo!
      upload: Carica
    dashboard:
      backlog: lavori arretrati
      config: Configurazione
      feature_deletions: Cancellazioni di account
      feature_invites: Link di invito
      feature_profile_directory: Directory dei profili
      feature_registrations: Registrazioni
      feature_relay: Ripetitore di federazione
      feature_timeline_preview: Anteprima timeline
      features: Funzionalità
      hidden_service: Federazione con servizi nascosti
      open_reports: apri report
      recent_users: Utenti Recenti
      search: Ricerca testo intero
      single_user_mode: Modalita utente singolo
      software: Software
      space: Utilizzo dello spazio
      title: Cruscotto
      total_users: utenti totali
      trends: Tendenze
      week_interactions: interazioni per questa settimana


@@ 235,6 270,7 @@ it:
      created_msg: Il blocco del dominio sta venendo processato
      destroyed_msg: Il blocco del dominio è stato rimosso
      domain: Dominio
      existing_domain_block_html: Hai già impostato limitazioni più stringenti su %{name}, dovresti <a href="%{unblock_url}">sbloccare</a> prima.
      new:
        create: Crea blocco
        hint: Il blocco dominio non previene la creazione di utenti nel database, ma applicherà automaticamente e retroattivamente metodi di moderazione specifici su quegli account.


@@ 248,6 284,8 @@ it:
      reject_media_hint: Rimuovi i file media salvati in locale e blocca i download futuri. Irrilevante per le sospensioni
      reject_reports: Respingi rapporti
      reject_reports_hint: Ignora tutti i rapporti provenienti da questo dominio. Irrilevante per sospensioni
      rejecting_media: rigetta file media
      rejecting_reports: rigetta segnalazioni
      severity:
        silence: silenziato
        suspend: sospeso


@@ 276,16 314,19 @@ it:
      title: Seguaci di %{acct}
    instances:
      by_domain: Dominio
      delivery_available: Distribuzione disponibile
      known_accounts:
        one: "%{count} account noto"
        other: "%{count} account noti"
      moderation:
        all: Tutto
        limited: Limitato
        title: Moderazione
      title: Istanze conosciute
      total_blocked_by_us: Bloccato da noi
      total_followed_by_them: Seguito da loro
      total_followed_by_us: Seguito da noi
      total_reported: Segnalazioni su di loro
      total_storage: Media allegati
    invites:
      deactivate_all: Disattiva tutto


@@ 295,6 336,8 @@ it:
        expired: Scaduto
        title: Filtro
      title: Inviti
    pending_accounts:
      title: Account in attesa (%{count})
    relays:
      add_new: Aggiungi ripetitore
      delete: Cancella


@@ 308,12 351,14 @@ it:
      pending: In attesa dell'approvazione del ripetitore
      save_and_enable: Salva e attiva
      setup: Crea una connessione con un ripetitore
      status: Stato
      title: Ripetitori
    report_notes:
      created_msg: Nota rapporto creata!
      destroyed_msg: Nota rapporto cancellata!
    reports:
      account:
        note: note
        report: rapporto
      action_taken_by: Azione intrapresa da
      are_you_sure: Sei sicuro?


@@ 349,6 394,7 @@ it:
        desc_html: Separa i nomi utente con virgola. Funziona solo con account locali e non bloccati. Quando vuoto, valido per tutti gli amministratori locali.
        title: Seguiti predefiniti per i nuovi utenti
      contact_information:
        email: E-mail di lavoro
        username: Nome utente del contatto
      custom_css:
        desc_html: Modifica l'aspetto con il CSS caricato in ogni pagina


@@ 378,10 424,17 @@ it:
        min_invite_role:
          disabled: Nessuno
          title: Permetti inviti da
      registrations_mode:
        modes:
          approved: Approvazione richiesta per le iscrizioni
          none: Nessuno può iscriversi
          open: Chiunque può iscriversi
        title: Modalità di registrazione
      show_known_fediverse_at_about_page:
        desc_html: Quando attivato, mostra nell'anteprima i toot da tutte le istanze conosciute. Altrimenti mostra solo i toot locali.
        title: Mostra la fediverse conosciuta nell'anteprima della timeline
      show_staff_badge:
        desc_html: Mostra un distintivo dello staff sulla pagina dell'utente
        title: Mostra badge staff
      site_description:
        desc_html: Paragrafo introduttivo nella pagina iniziale. Descrive ciò che rende speciale questo server Mastodon e qualunque altra cosa sia importante dire. Potete usare marcatori HTML, in particolare <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.


@@ 410,6 463,8 @@ it:
        nsfw_off: Segna come non sensibile
        nsfw_on: Segna come sensibile
      failed_to_execute: Impossibile eseguire
      media:
        title: Media
      no_media: Nessun media
      no_status_selected: Nessun status è stato modificato perché nessuno era stato selezionato
      title: Gli status dell'account


@@ 418,11 473,14 @@ it:
      callback_url: URL Callback
      confirmed: Confermato
      expires_in: Scade in
      last_delivery: Ultima distribuzione
      title: WebSub
      topic: Argomento
    tags:
      accounts: Account
      hidden: Nascosto
      hide: Non mostrare nella directory
      hide: Nascondi dalla directory
      name: Etichetta
      title: Hashtag
      unhide: Mostra nella directory
      visible: Visibile


@@ 433,8 491,25 @@ it:
      edit: Modifica
      edit_preset: Modifica avviso predefinito
      title: Gestisci avvisi predefiniti
  admin_mailer:
    new_pending_account:
      body: I dettagli del nuovo account sono qui sotto. Puoi approvare o rifiutare questa richiesta.
      subject: Nuovo account pronto per la revisione su %{instance} (%{username})
    new_report:
      body: "%{reporter} ha segnalato %{target}"
      body_remote: Qualcuno da %{domain} ha segnalato %{target}
      subject: Nuova segnalazione per %{instance} (#%{id})
  appearance:
    advanced_web_interface: Interfaccia web avanzata
    advanced_web_interface_hint: |-
      Se vuoi utilizzare l'intera larghezza dello schermo, l'interfaccia web avanzata ti consente di configurare varie colonne per mostrare più informazioni allo stesso tempo, secondo le tue preferenze:
      Home, notifiche, timeline federata, qualsiasi numero di liste e etichette.
    animations_and_accessibility: Animazioni e accessibiiltà
    confirmation_dialogs: Dialoghi di conferma
    sensitive_content: Contenuto sensibile
  application_mailer:
    notification_preferences: Cambia preferenze email
    salutation: "%{name},"
    settings: 'Cambia le impostazioni per le email: %{link}'
    view: 'Guarda:'
    view_profile: Mostra profilo


@@ 446,22 521,32 @@ it:
    regenerate_token: Rigenera il token di accesso
    token_regenerated: Token di accesso rigenerato
    warning: Fa' molta attenzione con questi dati. Non fornirli mai a nessun altro!
    your_token: Il tuo token di accesso
  auth:
    apply_for_account: Richiedi un invito
    change_password: Password
    checkbox_agreement_html: Sono d'accordo con le <a href="%{rules_path}" target="_blank">regole del server</a> ed i <a href="%{terms_path}" target="_blank">termini di servizio</a>
    confirm_email: Conferma email
    delete_account: Elimina account
    delete_account_html: Se desideri cancellare il tuo account, puoi <a href="%{path}">farlo qui</a>. Ti sarà chiesta conferma.
    didnt_get_confirmation: Non hai ricevuto le istruzioni di conferma?
    forgot_password: Hai dimenticato la tua password?
    invalid_reset_password_token: Il token di reimpostazione della password non è valido o è scaduto. Per favore richiedine uno nuovo.
    login: Entra
    logout: Esci da Mastodon
    migrate_account: Sposta ad un account differente
    migrate_account_html: Se vuoi che questo account sia reindirizzato a uno diverso, puoi <a href="%{path}">configurarlo qui</a>.
    or_log_in_with: Oppure accedi con
    providers:
      cas: CAS
      saml: SAML
    register: Iscriviti
    registration_closed: "%{instance} non accetta nuovi membri"
    resend_confirmation: Invia di nuovo le istruzioni di conferma
    reset_password: Resetta la password
    security: Credenziali
    set_new_password: Imposta una nuova password
    trouble_logging_in: Problemi di accesso?
  authorize_follow:
    already_following: Stai già seguendo questo account
    error: Sfortunatamente c'è stato un errore nel consultare l'account remoto


@@ 494,6 579,7 @@ it:
    proceed: Cancella l'account
    success_msg: Il tuo account è stato cancellato
    warning_html: È garantita la cancellazione del contenuto solo da questo server. I contenuti che sono stati ampiamente condivisi probabilmente lasceranno delle tracce. I server offline e quelli che non ricevono più i tuoi aggiornamenti non aggiorneranno i loro database.
    warning_title: Disponibilità di contenuto diffuso
  directories:
    directory: Directory dei profili
    enabled: Attualmente sei elencato nella directory.


@@ 511,11 597,14 @@ it:
    '422':
      content: Verifica di sicurezza non riuscita. Stai bloccando i cookies?
      title: Verifica di sicurezza non riuscita
    '429': Throttled
    '429': Limitato
    '500':
      content: Siamo spiacenti, ma qualcosa non ha funzionato dal nostro lato.
      title: Questa pagina non è corretta
    noscript_html: Per usare l'interfaccia web di Mastodon dovi abilitare JavaScript. In alternativa puoi provare una delle <a href="%{apps_path}">app native</a> per Mastodon per la tua piattaforma.
  existing_username_validator:
    not_found: impossibile trovare un utente locale con quel nome utente
    not_found_multiple: impossibile trovare %{usernames}
  exports:
    archive_takeout:
      date: Data


@@ 525,6 614,7 @@ it:
      request: Richiedi il tuo archivio
      size: Dimensioni
    blocks: Stai bloccando
    csv: CSV
    domain_blocks: Blocchi di dominio
    follows: Stai seguendo
    lists: Liste


@@ 544,6 634,7 @@ it:
      title: Modifica filtro
    errors:
      invalid_context: Contesto mancante o non valido
      invalid_irreversible: Il filtraggio irreversibile funziona solo nei contesti di home o notifiche
    index:
      delete: Cancella
      title: Filtri


@@ 552,13 643,36 @@ it:
  footer:
    developers: Sviluppatori
    more: Altro…
    resources: Risorse
  generic:
    all: Tutto
    changes_saved_msg: Modifiche effettuate con successo!
    copy: Copia
    order_by: Ordina per
    save_changes: Salva modifiche
    validation_errors:
      one: Qualcosa ancora non va bene! Per favore, controlla l'errore qui sotto
      other: Qualcosa ancora non va bene! Per favore, controlla i %{count} errori qui sotto
  html_validator:
    invalid_markup: 'contiene markup HTML non valido: %{error}'
  identity_proofs:
    active: Attive
    authorize: Si, autorizza
    authorize_connection_prompt: Autorizzare questa connessione crittografata?
    errors:
      failed: La connessione crittografata non è riuscita. Per favore riprova da %{provider}.
      keybase:
        invalid_token: I toked di Keybase sono hash di firme e devono essere lunghi 66 caratteri esadecimali
        verification_failed: Keybase non riconosce questo token come firma dell'utente Keybase %{kb_username}. Per favore riprova da Keybase.
      wrong_user: Impossibile creare una prova per %{proving} mentre si è effettuato l'accesso come %{current}. Accedi come %{proving} e riprova.
    explanation_html: Qui puoi connettere crittograficamente le tue altre identità, come il profilo Keybase. Questo consente ad altre persone di inviarti messaggi criptati e fidarsi dei contenuto che tu invii a loro.
    i_am_html: Io sono %{username} su %{service}.
    identity: Identità
    inactive: Inattiva
    publicize_checkbox: 'E posta questo:'
    publicize_toot: 'É provato! Io sono %{username} su %{service}: %{url}'
    status: Stato della verifica
    view_proof: Vedi prova
  imports:
    modes:
      merge: Fondi


@@ 573,6 687,7 @@ it:
      following: Lista dei seguaci
      muting: Lista dei silenziati
    upload: Carica
  in_memoriam_html: In Memoriam.
  invites:
    delete: Disattiva
    expired: Scaduto


@@ 643,14 758,26 @@ it:
      body: 'Il tuo status è stato condiviso da %{name}:'
      subject: "%{name} ha condiviso il tuo status"
      title: Nuova condivisione
  number:
    human:
      decimal_units:
        format: "%n%u"
        units:
          billion: G
          million: M
          quadrillion: P
          thousand: k
          trillion: T
  pagination:
    newer: Più recente
    next: Avanti
    older: Più vecchio
    prev: Indietro
    truncate: "&hellip;"
  polls:
    errors:
      already_voted: Hai già votato in questo sondaggio
      duplicate_options: contiene oggetti duplicati
      duration_too_long: è troppo lontano nel futuro
      duration_too_short: è troppo presto
      expired: Il sondaggio si è già concluso


@@ 659,12 786,28 @@ it:
      too_many_options: non può contenere più di %{max} elementi
  preferences:
    other: Altro
    posting_defaults: Predefinite di pubblicazione
    public_timelines: Timeline pubbliche
  relationships:
    activity: Attività dell'account
    dormant: Dormiente
    last_active: Ultima volta attivo
    most_recent: Più recente
    moved: Trasferito
    mutual: Reciproco
    primary: Principale
    relationship: Relazione
    remove_selected_domains: Rimuovi tutti i seguaci dai domini selezionati
    remove_selected_followers: Rimuovi i seguaci selezionati
    remove_selected_follows: Smetti di seguire gli utenti selezionati
    status: Stato dell'account
  remote_follow:
    acct: Inserisci il tuo username@dominio da cui vuoi seguire questo utente
    missing_resource: Impossibile trovare l'URL di reindirizzamento richiesto per il tuo account
    no_account_html: Non hai un account? Puoi <a href='%{sign_up_path}' target='_blank'>iscriverti qui</a>
    proceed: Conferma
    prompt: 'Stai per seguire:'
    reason_html: "<strong>Perchè questo passo è necessario?</strong> <code>%{instance}</code> potrebbe non essere il server nel quale tu sei registrato, quindi dobbiamo reindirizzarti prima al tuo server."
  remote_interaction:
    favourite:
      proceed: Continua per segnare come apprezzato


@@ 685,15 828,49 @@ it:
    too_soon: La data di pubblicazione deve essere nel futuro
  sessions:
    activity: Ultima attività
    browser: Browser
    browsers:
      alipay: Alipay
      blackberry: Blackberry
      chrome: Chrome
      edge: Microsoft Edge
      electron: Electron
      firefox: Firefox
      generic: Browser sconosciuto
      ie: Internet Explorer
      micro_messenger: MicroMessenger
      nokia: Nokia S40 Ovi Browser
      opera: Opera
      otter: Otter
      phantom_js: PhantomJS
      qq: QQ Browser
      safari: Safari
      uc_browser: UCBrowser
      weibo: Weibo
    current_session: Sessione corrente
    description: "%{browser} su %{platform}"
    explanation: Questi sono i browser da cui attualmente è avvenuto l'accesso al tuo account Mastodon.
    ip: IP
    platforms:
      adobe_air: Adobe Air
      android: Android
      blackberry: Blackberry
      chrome_os: ChromeOS
      firefox_os: Firefox OS
      ios: iOS
      linux: Linux
      mac: Mac
      other: piattaforma sconosciuta
      windows: Windows
      windows_mobile: Windows Mobile
      windows_phone: Windows Phone
    revoke: Revoca
    revoke_success: Sessione revocata con successo
    title: Sessioni
  settings:
    account: Account
    account_settings: Impostazioni dell'account
    appearance: Interfaccia
    authorized_apps: Applicazioni autorizzate
    back: Torna a Mastodon
    delete: Cancellazione account


@@ 701,10 878,14 @@ it:
    edit_profile: Modifica profilo
    export: Esporta impostazioni
    featured_tags: Hashtag in evidenza
    identity_proofs: Prove di identità
    import: Importa
    import_and_export: Importa ed esporta
    migrate: Migrazione dell'account
    notifications: Notifiche
    preferences: Preferenze
    profile: Profilo
    relationships: Follows and followers
    two_factor_authentication: Autenticazione a due fattori
  statuses:
    attached:


@@ 712,7 893,11 @@ it:
      image:
        one: "%{count} immagine"
        other: "%{count} immagini"
      video:
        one: "%{count} video"
        other: "%{count} video"
    boosted_from_html: Condiviso da %{acct_link}
    content_warning: 'Avviso di contenuto: %{warning}'
    disallowed_hashtags:
      one: 'contiene un hashtag non permesso: %{tags}'
      other: 'contiene gli hashtags non permessi: %{tags}'


@@ 731,6 916,7 @@ it:
      vote: Vota
    show_more: Mostra di più
    sign_in_to_participate: Accedi per partecipare alla conversazione
    title: '%{name}: "%{quote}"'
    visibilities:
      private: Mostra solo ai tuoi seguaci
      private_long: Mostra solo ai seguaci


@@ 748,6 934,10 @@ it:
    contrast: Mastodon (contrasto elevato)
    default: Mastodon (scuro)
    mastodon-light: Mastodon (chiaro)
  time:
    formats:
      default: "%b %d, %Y, %H:%M"
      month: "%b %Y"
  two_factor_authentication:
    code_hint: Inserisci il codice generato dalla tua app di autenticazione
    description_html: Se abiliti <strong>l'autorizzazione a due fattori</strong>, entrare nel tuo account ti richiederà di avere vicino il tuo telefono, il quale ti genererà un codice per eseguire l'accesso.


@@ 769,7 959,24 @@ it:
      explanation: Hai richiesto un backup completo del tuo account Mastodon. È pronto per essere scaricato!
      subject: Il tuo archivio è pronto per essere scaricato
      title: Esportazione archivio
    warning:
      explanation:
        disable: Mentre il tuo account è congelato, i tuoi dati dell'account rimangono intatti, ma non potrai eseguire nessuna azione fintanto che non viene sbloccato.
        silence: Mentre il tuo account è limitato, solo le persone che già ti seguono possono vedere i tuoi toot su questo server, e potresti essere escluso da vari elenchi pubblici. Comunque, altri possono manualmente seguirti.
        suspend: Il tuo account è stato sospeso, e tutti i tuoi toot ed i tuoi file media caricati sono stati irreversibilmente rimossi da questo server, e dai server dove avevi dei seguaci.
      review_server_policies: Rivedi regole del server
      subject:
        disable: Il tuo account %{acct} è stato congelato
        none: Avviso per %{acct}
        silence: Il tuo account %{acct} è stato limitato
        suspend: Il tuo account %{acct} è stato sospeso
      title:
        disable: Account congelato
        none: Avviso
        silence: Account limitato
        suspend: Account sospeso
    welcome:
      edit_profile_action: Imposta profilo
      edit_profile_step: Puoi personalizzare il tuo profilo caricando un avatar, un'intestazione, modificando il tuo nome visualizzato e così via. Se vuoi controllare i tuoi nuovi seguaci prima di autorizzarli a seguirti, puoi bloccare il tuo account.
      explanation: Ecco alcuni suggerimenti per iniziare
      final_action: Inizia a postare


@@ 789,6 996,7 @@ it:
    follow_limit_reached: Non puoi seguire più di %{limit} persone
    invalid_email: L'indirizzo email inserito non è valido
    invalid_otp_token: Codice d'accesso non valido
    otp_lost_help_html: Se perdessi l'accesso ad entrambi, puoi entrare in contatto con %{email}
    seamless_external_login: Ti sei collegato per mezzo di un servizio esterno, quindi le impostazioni di email e password non sono disponibili.
    signed_in_as: 'Hai effettuato l''accesso come:'
  verification:

M config/locales/pl.yml => config/locales/pl.yml +1 -1
@@ 35,7 35,7 @@ pl:
      one: wpisu
      other: wpisów
    status_count_before: Są autorami
    tagline: Śledź znajomych i poznawal nowych
    tagline: Śledź znajomych i poznawaj nowych
    terms: Zasady użytkowania
    user_count_after:
      few: użytkowników

M config/locales/simple_form.it.yml => config/locales/simple_form.it.yml +15 -0
@@ 27,6 27,7 @@ it:
        phrase: Il confronto sarà eseguito ignorando minuscole/maiuscole e i content warning
        scopes: A quali API l'applicazione potrà avere accesso. Se selezionate un ambito di alto livello, non c'è bisogno di selezionare quelle singole.
        setting_aggregate_reblogs: Non mostrare nuove condivisioni per toot che sono stati condivisi di recente (ha effetto solo sulle nuove condivisioni)
        setting_default_sensitive: Media con contenuti sensibili sono nascosti in modo predefinito e possono essere rivelati con un click
        setting_display_media_default: Nascondi media segnati come sensibili
        setting_display_media_hide_all: Nascondi sempre tutti i media
        setting_display_media_show_all: Nascondi sempre i media segnati come sensibili


@@ 39,6 40,8 @@ it:
        name: 'Eccone alcuni che potresti usare:'
      imports:
        data: File CSV esportato da un altro server Mastodon
      invite_request:
        text: Questo ci aiuterà ad esaminare la tua richiesta
      sessions:
        otp: 'Inserisci il codice a due fattori generato dall''app del tuo telefono o usa uno dei codici di recupero:'
      user:


@@ 62,12 65,14 @@ it:
        warning_preset_id: Usa un avviso preimpostato
      defaults:
        autofollow: Invita a seguire il tuo account
        avatar: Immagine di profilo
        bot: Questo account è un bot
        chosen_languages: Filtra lingue
        confirm_new_password: Conferma nuova password
        confirm_password: Conferma password
        context: Contesti del filtro
        current_password: Password corrente
        data: Data
        discoverable: Inserisci questo account nella directory
        display_name: Nome visualizzato
        email: Indirizzo email


@@ 82,7 87,9 @@ it:
        new_password: Nuova password
        note: Biografia
        otp_attempt: Codice due-fattori
        password: Password
        phrase: Parola chiave o frase
        setting_advanced_layout: Abilita interfaccia web avanzata
        setting_aggregate_reblogs: Raggruppa condivisioni in timeline
        setting_auto_play_gif: Play automatico GIF animate
        setting_boost_modal: Mostra dialogo di conferma prima del boost


@@ 107,18 114,26 @@ it:
        username: Nome utente
        username_or_email: Nome utente o email
        whole_word: Parola intera
      featured_tag:
        name: Etichetta
      interactions:
        must_be_follower: Blocca notifiche da chi non ti segue
        must_be_following: Blocca notifiche dalle persone che non segui
        must_be_following_dm: Blocca i messaggi diretti dalle persone che non segui
      invite_request:
        text: Perchè vuoi unirti?
      notification_emails:
        digest: Invia email riassuntive
        favourite: Invia email quando segna come preferito al tuo stato
        follow: Invia email quando qualcuno ti segue
        follow_request: Invia email quando qualcuno richiede di seguirti
        mention: Invia email quando qualcuno ti menziona
        pending_account: Invia e-mail quando un nuovo account richiede l'approvazione
        reblog: Invia email quando qualcuno da un boost al tuo stato
        report: Manda una mail quando viene inviato un nuovo rapporto
    'no': 'No'
    recommended: Consigliato
    required:
      mark: "*"
      text: richiesto
    'yes': Si

M config/locales/simple_form.ja.yml => config/locales/simple_form.ja.yml +1 -0
@@ 135,5 135,6 @@ ja:
    'no': いいえ
    recommended: おすすめ
    required:
      mark: "*"
      text: 必須
    'yes': はい

M config/locales/sk.yml => config/locales/sk.yml +4 -0
@@ 546,6 546,7 @@ sk:
    migrate_account_html: Ak si želáš presmerovať tento účet na nejaký iný, môžeš si to <a href="%{path}">nastaviť tu</a>.
    or_log_in_with: Alebo prihlás s
    register: Zaregistruj sa
    registration_closed: "%{instance} neprijíma nových členov"
    resend_confirmation: Zašli potvrdzujúce pokyny znovu
    reset_password: Obnov heslo
    security: Zabezpečenie


@@ 668,6 669,7 @@ sk:
    publicize_checkbox: 'A poslať toto:'
    publicize_toot: 'Je to dokázané! Na %{service} som %{username}: %{url}'
    status: Stav overenia
    view_proof: Ukáž overenie
  imports:
    modes:
      merge: Spoj dohromady


@@ 766,6 768,7 @@ sk:
      too_many_options: nemôže zahŕňať viac ako %{max} položiek
  preferences:
    other: Ostatné
    public_timelines: Verejné časové osi
  relationships:
    activity: Aktivita účtu
    dormant: Spiace


@@ 773,6 776,7 @@ sk:
    most_recent: Najnovšie
    moved: Presunuli sa
    mutual: Spoločné
    primary: Hlavné
    relationship: Vzťah
    remove_selected_followers: Odstráň vybraných následovatrľov
    remove_selected_follows: Prestaň sledovať vybraných užívateľov

M config/routes.rb => config/routes.rb +23 -0
@@ 409,6 409,29 @@ Rails.application.routes.draw do
      namespace :push do
        resource :subscription, only: [:create, :show, :update, :destroy]
      end

      namespace :admin do
        resources :accounts, only: [:index, :show] do
          member do
            post :enable
            post :unsilence
            post :unsuspend
            post :approve
            post :reject
          end

          resource :action, only: [:create], controller: 'account_actions'
        end

        resources :reports, only: [:index, :show] do
          member do
            post :assign_to_self
            post :unassign
            post :reopen
            post :resolve
          end
        end
      end
    end

    namespace :v2 do

M lib/mastodon/version.rb => lib/mastodon/version.rb +1 -1
@@ 13,7 13,7 @@ module Mastodon
    end

    def patch
      0
      2
    end

    def pre

D lib/paperclip/audio_transcoder.rb => lib/paperclip/audio_transcoder.rb +0 -23
@@ 1,23 0,0 @@
# frozen_string_literal: true

module Paperclip
  class AudioTranscoder < Paperclip::Processor
    def make
      max_aud_len = (ENV['MAX_AUDIO_LENGTH'] || 60.0).to_f

      meta = ::Av.cli.identify(@file.path)
      # {:length=>"0:00:02.14", :duration=>2.14, :audio_encode=>"mp3", :audio_bitrate=>"44100 Hz", :audio_channels=>"mono"}
      if meta[:duration] > max_aud_len
        raise Mastodon::ValidationError, "Audio uploads must be less than #{max_aud_len} seconds in length."
      end
      
      final_file = Paperclip::Transcoder.make(file, options, attachment)
      
      attachment.instance.file_file_name    = 'media.mp4'
      attachment.instance.file_content_type = 'video/mp4'
      attachment.instance.type              = MediaAttachment.types[:video]

      final_file
    end
  end
end

A lib/paperclip/type_corrector.rb => lib/paperclip/type_corrector.rb +19 -0
@@ 0,0 1,19 @@
# frozen_string_literal: true

require 'mime/types/columnar'

module Paperclip
  class TypeCorrector < Paperclip::Processor
    def make
      target_extension = options[:format]
      extension        = File.extname(attachment.instance.file_file_name)

      return @file unless options[:style] == :original && target_extension && extension != target_extension

      attachment.instance.file_content_type = options[:content_type] || attachment.instance.file_content_type
      attachment.instance.file_file_name    = File.basename(attachment.instance.file_file_name, '.*') + '.' + target_extension

      @file
    end
  end
end

A spec/controllers/api/v1/admin/account_actions_controller_spec.rb => spec/controllers/api/v1/admin/account_actions_controller_spec.rb +57 -0
@@ 0,0 1,57 @@
require 'rails_helper'

RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do
  render_views

  let(:role)   { 'moderator' }
  let(:user)   { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) }
  let(:scopes) { 'admin:read admin:write' }
  let(:token)  { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
  let(:account) { Fabricate(:user).account }

  before do
    allow(controller).to receive(:doorkeeper_token) { token }
  end

  shared_examples 'forbidden for wrong scope' do |wrong_scope|
    let(:scopes) { wrong_scope }

    it 'returns http forbidden' do
      expect(response).to have_http_status(403)
    end
  end

  shared_examples 'forbidden for wrong role' do |wrong_role|
    let(:role) { wrong_role }

    it 'returns http forbidden' do
      expect(response).to have_http_status(403)
    end
  end

  describe 'POST #create' do
    before do
      post :create, params: { account_id: account.id, type: 'disable' }
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'performs action against account' do
      expect(account.reload.user_disabled?).to be true
    end

    it 'logs action' do
      log_item = Admin::ActionLog.last

      expect(log_item).to_not be_nil
      expect(log_item.action).to eq :disable
      expect(log_item.account_id).to eq user.account_id
      expect(log_item.target_id).to eq account.user.id
    end
  end
end

A spec/controllers/api/v1/admin/accounts_controller_spec.rb => spec/controllers/api/v1/admin/accounts_controller_spec.rb +147 -0
@@ 0,0 1,147 @@
require 'rails_helper'

RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
  render_views

  let(:role)   { 'moderator' }
  let(:user)   { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) }
  let(:scopes) { 'admin:read admin:write' }
  let(:token)  { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
  let(:account) { Fabricate(:user).account }

  before do
    allow(controller).to receive(:doorkeeper_token) { token }
  end

  shared_examples 'forbidden for wrong scope' do |wrong_scope|
    let(:scopes) { wrong_scope }

    it 'returns http forbidden' do
      expect(response).to have_http_status(403)
    end
  end

  shared_examples 'forbidden for wrong role' do |wrong_role|
    let(:role) { wrong_role }

    it 'returns http forbidden' do
      expect(response).to have_http_status(403)
    end
  end

  describe 'GET #index' do
    before do
      get :index
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end
  end

  describe 'GET #show' do
    before do
      get :show, params: { id: account.id }
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end
  end

  describe 'POST #approve' do
    before do
      account.user.update(approved: false)
      post :approve, params: { id: account.id }
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'approves user' do
      expect(account.reload.user_approved?).to be true
    end
  end

  describe 'POST #reject' do
    before do
      account.user.update(approved: false)
      post :reject, params: { id: account.id }
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'removes user' do
      expect(User.where(id: account.user.id).count).to eq 0
    end
  end

  describe 'POST #enable' do
    before do
      account.user.update(disabled: true)
      post :enable, params: { id: account.id }
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'enables user' do
      expect(account.reload.user_disabled?).to be false
    end
  end

  describe 'POST #unsuspend' do
    before do
      account.touch(:suspended_at)
      post :unsuspend, params: { id: account.id }
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'unsuspends account' do
      expect(account.reload.suspended?).to be false
    end
  end

  describe 'POST #unsilence' do
    before do
      account.touch(:silenced_at)
      post :unsilence, params: { id: account.id }
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end

    it 'unsilences account' do
      expect(account.reload.silenced?).to be false
    end
  end
end

A spec/controllers/api/v1/admin/reports_controller_spec.rb => spec/controllers/api/v1/admin/reports_controller_spec.rb +109 -0
@@ 0,0 1,109 @@
require 'rails_helper'

RSpec.describe Api::V1::Admin::ReportsController, type: :controller do
  render_views

  let(:role)   { 'moderator' }
  let(:user)   { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) }
  let(:scopes) { 'admin:read admin:write' }
  let(:token)  { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
  let(:report) { Fabricate(:report) }

  before do
    allow(controller).to receive(:doorkeeper_token) { token }
  end

  shared_examples 'forbidden for wrong scope' do |wrong_scope|
    let(:scopes) { wrong_scope }

    it 'returns http forbidden' do
      expect(response).to have_http_status(403)
    end
  end

  shared_examples 'forbidden for wrong role' do |wrong_role|
    let(:role) { wrong_role }

    it 'returns http forbidden' do
      expect(response).to have_http_status(403)
    end
  end

  describe 'GET #index' do
    before do
      get :index
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end
  end

  describe 'GET #show' do
    before do
      get :show, params: { id: report.id }
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end
  end

  describe 'POST #resolve' do
    before do
      post :resolve, params: { id: report.id }
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end
  end

  describe 'POST #reopen' do
    before do
      post :reopen, params: { id: report.id }
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end
  end

  describe 'POST #assign_to_self' do
    before do
      post :assign_to_self, params: { id: report.id }
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end
  end

  describe 'POST #unassign' do
    before do
      post :unassign, params: { id: report.id }
    end

    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
    it_behaves_like 'forbidden for wrong role', 'user'

    it 'returns http success' do
      expect(response).to have_http_status(200)
    end
  end
end

M spec/models/account_spec.rb => spec/models/account_spec.rb +17 -0
@@ 687,6 687,23 @@ RSpec.describe Account, type: :model do
      end
    end

    describe 'by_domain_and_subdomains' do
      it 'returns exact domain matches' do
        account = Fabricate(:account, domain: 'example.com')
        expect(Account.by_domain_and_subdomains('example.com')).to eq [account]
      end

      it 'returns subdomains' do
        account = Fabricate(:account, domain: 'foo.example.com')
        expect(Account.by_domain_and_subdomains('example.com')).to eq [account]
      end

      it 'does not return partially matching domains' do
        account = Fabricate(:account, domain: 'grexample.com')
        expect(Account.by_domain_and_subdomains('example.com')).to_not eq [account]
      end
    end

    describe 'expiring' do
      it 'returns remote accounts with followers whose subscription expiration date is past or not given' do
        local = Fabricate(:account, domain: nil)

M spec/models/domain_block_spec.rb => spec/models/domain_block_spec.rb +24 -7
@@ 21,23 21,40 @@ RSpec.describe DomainBlock, type: :model do
    end
  end

  describe 'blocked?' do
  describe '.blocked?' do
    it 'returns true if the domain is suspended' do
      Fabricate(:domain_block, domain: 'domain', severity: :suspend)
      expect(DomainBlock.blocked?('domain')).to eq true
      Fabricate(:domain_block, domain: 'example.com', severity: :suspend)
      expect(DomainBlock.blocked?('example.com')).to eq true
    end

    it 'returns false even if the domain is silenced' do
      Fabricate(:domain_block, domain: 'domain', severity: :silence)
      expect(DomainBlock.blocked?('domain')).to eq false
      Fabricate(:domain_block, domain: 'example.com', severity: :silence)
      expect(DomainBlock.blocked?('example.com')).to eq false
    end

    it 'returns false if the domain is not suspended nor silenced' do
      expect(DomainBlock.blocked?('domain')).to eq false
      expect(DomainBlock.blocked?('example.com')).to eq false
    end
  end

  describe 'stricter_than?' do
  describe '.rule_for' do
    it 'returns rule matching a blocked domain' do
      block = Fabricate(:domain_block, domain: 'example.com')
      expect(DomainBlock.rule_for('example.com')).to eq block
    end

    it 'returns a rule matching a subdomain of a blocked domain' do
      block = Fabricate(:domain_block, domain: 'example.com')
      expect(DomainBlock.rule_for('sub.example.com')).to eq block
    end

    it 'returns a rule matching a blocked subdomain' do
      block = Fabricate(:domain_block, domain: 'sub.example.com')
      expect(DomainBlock.rule_for('sub.example.com')).to eq block
    end
  end

  describe '#stricter_than?' do
    it 'returns true if the new block has suspend severity while the old has lower severity' do
      suspend = DomainBlock.new(domain: 'domain', severity: :suspend)
      silence = DomainBlock.new(domain: 'domain', severity: :silence)