about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.env.production.sample5
-rw-r--r--CHANGELOG.md39
-rw-r--r--app/controllers/admin/accounts_controller.rb1
-rw-r--r--app/controllers/admin/domain_blocks_controller.rb2
-rw-r--r--app/controllers/admin/instances_controller.rb2
-rw-r--r--app/controllers/api/v1/admin/account_actions_controller.rb32
-rw-r--r--app/controllers/api/v1/admin/accounts_controller.rb128
-rw-r--r--app/controllers/api/v1/admin/reports_controller.rb108
-rw-r--r--app/controllers/media_controller.rb12
-rw-r--r--app/controllers/media_proxy_controller.rb2
-rw-r--r--app/controllers/settings/identity_proofs_controller.rb4
-rw-r--r--app/javascript/flavours/glitch/components/media_gallery.js2
-rw-r--r--app/javascript/flavours/glitch/components/status.js14
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/compose_form.js4
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/options_container.js2
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js14
-rw-r--r--app/javascript/flavours/glitch/styles/components/composer.scss1
-rw-r--r--app/javascript/mastodon/components/media_gallery.js2
-rw-r--r--app/javascript/mastodon/components/status.js12
-rw-r--r--app/javascript/mastodon/features/compose/components/upload_button.js8
-rw-r--r--app/javascript/mastodon/features/compose/containers/upload_button_container.js2
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js12
-rw-r--r--app/javascript/mastodon/locales/ar.json2
-rw-r--r--app/javascript/mastodon/locales/ca.json4
-rw-r--r--app/javascript/mastodon/locales/de.json2
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json2
-rw-r--r--app/javascript/mastodon/locales/en.json2
-rw-r--r--app/javascript/mastodon/locales/fi.json58
-rw-r--r--app/javascript/mastodon/locales/it.json56
-rw-r--r--app/javascript/mastodon/locales/ja.json2
-rw-r--r--app/javascript/mastodon/locales/nl.json12
-rw-r--r--app/javascript/mastodon/locales/sk.json2
-rw-r--r--app/javascript/mastodon/locales/sl.json346
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json2
-rw-r--r--app/javascript/styles/mastodon/components.scss1
-rw-r--r--app/lib/activitypub/activity/create.rb4
-rw-r--r--app/lib/activitypub/activity/flag.rb2
-rw-r--r--app/lib/ostatus/activity/creation.rb4
-rw-r--r--app/models/account.rb3
-rw-r--r--app/models/account_filter.rb2
-rw-r--r--app/models/concerns/attachmentable.rb19
-rw-r--r--app/models/concerns/user_roles.rb14
-rw-r--r--app/models/custom_emoji.rb1
-rw-r--r--app/models/domain_block.rb33
-rw-r--r--app/models/instance.rb10
-rw-r--r--app/models/media_attachment.rb104
-rw-r--r--app/models/report.rb3
-rw-r--r--app/models/report_filter.rb2
-rw-r--r--app/models/user.rb1
-rw-r--r--app/serializers/initial_state_serializer.rb2
-rw-r--r--app/serializers/rest/admin/account_serializer.rb77
-rw-r--r--app/serializers/rest/admin/report_serializer.rb16
-rw-r--r--app/serializers/rest/instance_serializer.rb12
-rw-r--r--app/services/activitypub/process_account_service.rb2
-rw-r--r--app/services/block_domain_service.rb4
-rw-r--r--app/services/post_status_service.rb2
-rw-r--r--app/services/resolve_account_service.rb2
-rw-r--r--app/services/unblock_domain_service.rb3
-rw-r--r--app/services/update_remote_profile_service.rb4
-rw-r--r--app/views/admin/instances/index.html.haml21
-rw-r--r--app/views/stream_entries/_detailed_status.html.haml2
-rw-r--r--app/views/stream_entries/_simple_status.html.haml2
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/doorkeeper.rb8
-rw-r--r--config/locales/activerecord.fi.yml12
-rw-r--r--config/locales/activerecord.it.yml6
-rw-r--r--config/locales/devise.it.yml3
-rw-r--r--config/locales/doorkeeper.ca.yml6
-rw-r--r--config/locales/doorkeeper.co.yml6
-rw-r--r--config/locales/doorkeeper.cs.yml6
-rw-r--r--config/locales/doorkeeper.de.yml6
-rw-r--r--config/locales/doorkeeper.el.yml6
-rw-r--r--config/locales/doorkeeper.en.yml6
-rw-r--r--config/locales/doorkeeper.gl.yml6
-rw-r--r--config/locales/doorkeeper.it.yml8
-rw-r--r--config/locales/doorkeeper.ja.yml6
-rw-r--r--config/locales/doorkeeper.pl.yml6
-rw-r--r--config/locales/doorkeeper.sl.yml6
-rw-r--r--config/locales/doorkeeper.zh-CN.yml6
-rw-r--r--config/locales/it.yml216
-rw-r--r--config/locales/pl.yml2
-rw-r--r--config/locales/simple_form.it.yml15
-rw-r--r--config/locales/simple_form.ja.yml1
-rw-r--r--config/locales/sk.yml4
-rw-r--r--config/routes.rb23
-rw-r--r--lib/mastodon/version.rb2
-rw-r--r--lib/paperclip/audio_transcoder.rb23
-rw-r--r--lib/paperclip/type_corrector.rb19
-rw-r--r--spec/controllers/api/v1/admin/account_actions_controller_spec.rb57
-rw-r--r--spec/controllers/api/v1/admin/accounts_controller_spec.rb147
-rw-r--r--spec/controllers/api/v1/admin/reports_controller_spec.rb109
-rw-r--r--spec/models/account_spec.rb17
-rw-r--r--spec/models/domain_block_spec.rb31
93 files changed, 1605 insertions, 416 deletions
diff --git a/.env.production.sample b/.env.production.sample
index a64959c77..3388d380a 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c89f35cdf..539fec531 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
 
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index b0d45ce47..0c7760d77 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -127,6 +127,7 @@ module Admin
         :by_domain,
         :active,
         :pending,
+        :disabled,
         :silenced,
         :suspended,
         :username,
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index 71597763b..377cac8ad 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -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
diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb
index 6dd659a30..7888e844f 100644
--- a/app/controllers/admin/instances_controller.rb
+++ b/app/controllers/admin/instances_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/admin/account_actions_controller.rb b/app/controllers/api/v1/admin/account_actions_controller.rb
new file mode 100644
index 000000000..29c9b7107
--- /dev/null
+++ b/app/controllers/api/v1/admin/account_actions_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb
new file mode 100644
index 000000000..c306180ca
--- /dev/null
+++ b/app/controllers/api/v1/admin/accounts_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb
new file mode 100644
index 000000000..1d48d3160
--- /dev/null
+++ b/app/controllers/api/v1/admin/reports_controller.rb
@@ -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
diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb
index a245db2d1..d44b52d26 100644
--- a/app/controllers/media_controller.rb
+++ b/app/controllers/media_controller.rb
@@ -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
diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb
index 950cf6d09..8fc18dd06 100644
--- a/app/controllers/media_proxy_controller.rb
+++ b/app/controllers/media_proxy_controller.rb
@@ -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
diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb
index 4d0938545..e84c1aca6 100644
--- a/app/controllers/settings/identity_proofs_controller.rb
+++ b/app/controllers/settings/identity_proofs_controller.rb
@@ -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
diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js
index 6ef101f11..291caff45 100644
--- a/app/javascript/flavours/glitch/components/media_gallery.js
+++ b/app/javascript/flavours/glitch/components/media_gallery.js
@@ -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>
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index f6d73475a..ed2623ebb 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -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}>
diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
index cbce675d5..822cfa95d 100644
--- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
@@ -296,12 +296,12 @@ class ComposeForm extends ImmutablePureComponent {
     let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia);
 
     return (
-      <div className='composer' ref={this.setRef}>
+      <div className='composer'>
         <WarningContainer />
 
         <ReplyIndicatorContainer />
 
-        <div className={`composer--spoiler ${spoiler ? 'composer--spoiler--visible' : ''}`}>
+        <div className={`composer--spoiler ${spoiler ? 'composer--spoiler--visible' : ''}`} ref={this.setRef}>
           <AutosuggestInput
             placeholder={intl.formatMessage(messages.spoiler_placeholder)}
             value={spoilerText}
diff --git a/app/javascript/flavours/glitch/features/compose/containers/options_container.js b/app/javascript/flavours/glitch/features/compose/containers/options_container.js
index c8c7ecd43..df842f3bf 100644
--- a/app/javascript/flavours/glitch/features/compose/containers/options_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/options_container.js
@@ -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']),
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index ddedac4d4..1c2258256 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -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
diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss
index c06d79ffc..3eb5551c6 100644
--- a/app/javascript/flavours/glitch/styles/components/composer.scss
+++ b/app/javascript/flavours/glitch/styles/components/composer.scss
@@ -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;
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index 56618462b..77bac61ee 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -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>
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index aa5e870dc..9b1035649 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -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
diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js
index 90e2769f3..d550019f4 100644
--- a/app/javascript/mastodon/features/compose/components/upload_button.js
+++ b/app/javascript/mastodon/features/compose/components/upload_button.js
@@ -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}
diff --git a/app/javascript/mastodon/features/compose/containers/upload_button_container.js b/app/javascript/mastodon/features/compose/containers/upload_button_container.js
index d8b8c4b6e..1471e628b 100644
--- a/app/javascript/mastodon/features/compose/containers/upload_button_container.js
+++ b/app/javascript/mastodon/features/compose/containers/upload_button_container.js
@@ -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']),
 });
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index c7aa4d033..4af157af1 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -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
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 1586580a8..d05c61f98 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -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": "وصف للمعاقين بصريا",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index acb9709d0..bb73b2a41 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -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)",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 5c63af3b2..ac8bc9b9f 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index faca8241d..076aca2b1 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -1051,7 +1051,7 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)",
+        "defaultMessage": "Add media ({formats})",
         "id": "upload_button.label"
       }
     ],
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 49e4ddbda..a75c41799 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 4eca05ca5..342a15bfb 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -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ä",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index d5e88c4f3..f7e2e4353 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index bdc1f98fb..6dadf7c60 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -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": "視覚障害者のための説明",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 4bcab5ba0..f6504f4bb 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -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",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index d624aa25c..18993af97 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -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}",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index f0e813e90..51794a862 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -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}",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 05c29f16e..865d3a514 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -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}",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 597a8d1dc..dc750dbfe 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -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;
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index f55dd35b2..00f0dd42d 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -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?
diff --git a/app/lib/activitypub/activity/flag.rb b/app/lib/activitypub/activity/flag.rb
index f73b93058..1659bc61f 100644
--- a/app/lib/activitypub/activity/flag.rb
+++ b/app/lib/activitypub/activity/flag.rb
@@ -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
diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb
index 3840c8fbf..60de712db 100644
--- a/app/lib/ostatus/activity/creation.rb
+++ b/app/lib/ostatus/activity/creation.rb
@@ -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
 
diff --git a/app/models/account.rb b/app/models/account.rb
index 520b183e8..3d7b0dda3 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -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?,
diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
index d2503100c..c3b1fe08d 100644
--- a/app/models/account_filter.rb
+++ b/app/models/account_filter.rb
@@ -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'
diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb
index de4cf8775..24f5968de 100644
--- a/app/models/concerns/attachmentable.rb
+++ b/app/models/concerns/attachmentable.rb
@@ -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
diff --git a/app/models/concerns/user_roles.rb b/app/models/concerns/user_roles.rb
index 58dffdc46..a42b4a172 100644
--- a/app/models/concerns/user_roles.rb
+++ b/app/models/concerns/user_roles.rb
@@ -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'
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index d3cc70504..e73cd9bd2 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -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
 
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 84c08c158..25d3b87ef 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -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
 
diff --git a/app/models/instance.rb b/app/models/instance.rb
index 7bf000d40..797a191e0 100644
--- a/app/models/instance.rb
+++ b/app/models/instance.rb
@@ -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
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 70a671b4a..815ac0258 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -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
diff --git a/app/models/report.rb b/app/models/report.rb
index 86c303798..5192ceef7 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -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 }
 
diff --git a/app/models/report_filter.rb b/app/models/report_filter.rb
index 56ab28df7..a392d60c3 100644
--- a/app/models/report_filter.rb
+++ b/app/models/report_filter.rb
@@ -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
 
diff --git a/app/models/user.rb b/app/models/user.rb
index fcfb79612..f6936cb9d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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}%")) }
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index efed199f3..c46caa28e 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -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
diff --git a/app/serializers/rest/admin/account_serializer.rb b/app/serializers/rest/admin/account_serializer.rb
new file mode 100644
index 000000000..f579d3302
--- /dev/null
+++ b/app/serializers/rest/admin/account_serializer.rb
@@ -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
diff --git a/app/serializers/rest/admin/report_serializer.rb b/app/serializers/rest/admin/report_serializer.rb
new file mode 100644
index 000000000..7a77132c0
--- /dev/null
+++ b/app/serializers/rest/admin/report_serializer.rb
@@ -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
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index 98c53c84a..e913f0c64 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -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
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index ad22d37fe..05c017bdf 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -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?
diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb
index 497f0394b..c6eef04d4 100644
--- a/app/services/block_domain_service.rb
+++ b/app/services/block_domain_service.rb
@@ -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
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index c2584e090..6d7c44913 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -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)
diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb
index 11e33a83a..57c9ccfe1 100644
--- a/app/services/resolve_account_service.rb
+++ b/app/services/resolve_account_service.rb
@@ -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
diff --git a/app/services/unblock_domain_service.rb b/app/services/unblock_domain_service.rb
index 9b8526fbe..fc262a50a 100644
--- a/app/services/unblock_domain_service.rb
+++ b/app/services/unblock_domain_service.rb
@@ -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
diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb
index 68d36addf..403395a0d 100644
--- a/app/services/update_remote_profile_service.rb
+++ b/app/services/update_remote_profile_service.rb
@@ -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
 
diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml
index 9574c3147..61e578409 100644
--- a/app/views/admin/instances/index.html.haml
+++ b/app/views/admin/instances/index.html.haml
@@ -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
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index 95b96feef..069d0053f 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -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 }
diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml
index d383d3443..dcb4ce0b9 100644
--- a/app/views/stream_entries/_simple_status.html.haml
+++ b/app/views/stream_entries/_simple_status.html.haml
@@ -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 }
diff --git a/config/application.rb b/config/application.rb
index 150fdce6c..4534ede49 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -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'
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 2a963b32b..a5c9caa4a 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -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
diff --git a/config/locales/activerecord.fi.yml b/config/locales/activerecord.fi.yml
index 23c538b19..2b2ffd121 100644
--- a/config/locales/activerecord.fi.yml
+++ b/config/locales/activerecord.fi.yml
@@ -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
diff --git a/config/locales/activerecord.it.yml b/config/locales/activerecord.it.yml
index 4cec9fb63..70afdaef1 100644
--- a/config/locales/activerecord.it.yml
+++ b/config/locales/activerecord.it.yml
@@ -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:
diff --git a/config/locales/devise.it.yml b/config/locales/devise.it.yml
index fc36fdbff..b603e12c6 100644
--- a/config/locales/devise.it.yml
+++ b/config/locales/devise.it.yml
@@ -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.
diff --git a/config/locales/doorkeeper.ca.yml b/config/locales/doorkeeper.ca.yml
index dfa46551f..dde70f47a 100644
--- a/config/locales/doorkeeper.ca.yml
+++ b/config/locales/doorkeeper.ca.yml
@@ -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
diff --git a/config/locales/doorkeeper.co.yml b/config/locales/doorkeeper.co.yml
index 542ad7c57..d45041a4e 100644
--- a/config/locales/doorkeeper.co.yml
+++ b/config/locales/doorkeeper.co.yml
@@ -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
diff --git a/config/locales/doorkeeper.cs.yml b/config/locales/doorkeeper.cs.yml
index f523e125d..cb5cd147c 100644
--- a/config/locales/doorkeeper.cs.yml
+++ b/config/locales/doorkeeper.cs.yml
@@ -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
diff --git a/config/locales/doorkeeper.de.yml b/config/locales/doorkeeper.de.yml
index edf9c660c..c41a847b2 100644
--- a/config/locales/doorkeeper.de.yml
+++ b/config/locales/doorkeeper.de.yml
@@ -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
diff --git a/config/locales/doorkeeper.el.yml b/config/locales/doorkeeper.el.yml
index e820ff8a6..c63688ade 100644
--- a/config/locales/doorkeeper.el.yml
+++ b/config/locales/doorkeeper.el.yml
@@ -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: να διαβάζει όλα τα στοιχεία του λογαριασμού σου
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 211b210d7..4e9c83a8f 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -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
diff --git a/config/locales/doorkeeper.gl.yml b/config/locales/doorkeeper.gl.yml
index 0dc45d5a3..90cbd9b38 100644
--- a/config/locales/doorkeeper.gl.yml
+++ b/config/locales/doorkeeper.gl.yml
@@ -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
diff --git a/config/locales/doorkeeper.it.yml b/config/locales/doorkeeper.it.yml
index f6bd8b4bc..361d0bd75 100644
--- a/config/locales/doorkeeper.it.yml
+++ b/config/locales/doorkeeper.it.yml
@@ -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
diff --git a/config/locales/doorkeeper.ja.yml b/config/locales/doorkeeper.ja.yml
index 9bc2d9a80..d80212f82 100644
--- a/config/locales/doorkeeper.ja.yml
+++ b/config/locales/doorkeeper.ja.yml
@@ -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: アカウントのすべてのデータの読み取り
diff --git a/config/locales/doorkeeper.pl.yml b/config/locales/doorkeeper.pl.yml
index de724f6c9..2068eeef4 100644
--- a/config/locales/doorkeeper.pl.yml
+++ b/config/locales/doorkeeper.pl.yml
@@ -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
diff --git a/config/locales/doorkeeper.sl.yml b/config/locales/doorkeeper.sl.yml
index 341ed2830..26d92ddb5 100644
--- a/config/locales/doorkeeper.sl.yml
+++ b/config/locales/doorkeeper.sl.yml
@@ -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
diff --git a/config/locales/doorkeeper.zh-CN.yml b/config/locales/doorkeeper.zh-CN.yml
index 1cce6adc2..dd9337904 100644
--- a/config/locales/doorkeeper.zh-CN.yml
+++ b/config/locales/doorkeeper.zh-CN.yml
@@ -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: 读取你的帐户数据
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 1d1238056..6dfe212d1 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -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:
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 538e6d554..3285a26b6 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -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
diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml
index b832a8035..377a55293 100644
--- a/config/locales/simple_form.it.yml
+++ b/config/locales/simple_form.it.yml
@@ -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
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index 89f2e7a8d..dd7d9304d 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -135,5 +135,6 @@ ja:
     'no': いいえ
     recommended: おすすめ
     required:
+      mark: "*"
       text: 必須
     'yes': はい
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index 3e1255276..75a43e322 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -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
diff --git a/config/routes.rb b/config/routes.rb
index 0d49a07d6..05bcaf9e2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index 56b846a36..cd216b92d 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -13,7 +13,7 @@ module Mastodon
     end
 
     def patch
-      0
+      2
     end
 
     def pre
diff --git a/lib/paperclip/audio_transcoder.rb b/lib/paperclip/audio_transcoder.rb
deleted file mode 100644
index 323ec7bfe..000000000
--- a/lib/paperclip/audio_transcoder.rb
+++ /dev/null
@@ -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
diff --git a/lib/paperclip/type_corrector.rb b/lib/paperclip/type_corrector.rb
new file mode 100644
index 000000000..0b0c10a56
--- /dev/null
+++ b/lib/paperclip/type_corrector.rb
@@ -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
diff --git a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb
new file mode 100644
index 000000000..a5a8f4bb0
--- /dev/null
+++ b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb
@@ -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
diff --git a/spec/controllers/api/v1/admin/accounts_controller_spec.rb b/spec/controllers/api/v1/admin/accounts_controller_spec.rb
new file mode 100644
index 000000000..f3f9946ba
--- /dev/null
+++ b/spec/controllers/api/v1/admin/accounts_controller_spec.rb
@@ -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
diff --git a/spec/controllers/api/v1/admin/reports_controller_spec.rb b/spec/controllers/api/v1/admin/reports_controller_spec.rb
new file mode 100644
index 000000000..4ed3c5dc4
--- /dev/null
+++ b/spec/controllers/api/v1/admin/reports_controller_spec.rb
@@ -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
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 379872316..ce9ea250d 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -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)
diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb
index 0035fd0ff..d98c5e118 100644
--- a/spec/models/domain_block_spec.rb
+++ b/spec/models/domain_block_spec.rb
@@ -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)