about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2018-08-17 17:43:54 +0200
committerThibaut Girka <thib@sitedethib.com>2018-08-17 17:43:54 +0200
commit280d7b1df8566239f15130d8bf1e0e2c1d467fc0 (patch)
tree7903e071b0013fb15dcc5a74f6143239dcbaa2c2 /app
parent4a6bc2482a26aa0441485ca360a3d3c0eeea7fe4 (diff)
parent59f7f4c923494bb8dd6f2881a1610c7b51240d9c (diff)
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts:
	app/models/status.rb
	db/migrate/20180528141303_fix_accounts_unique_index.rb
	db/schema.rb

Resolved by taking upstream changes (no real conflicts, just glitch-soc
specific code too close to actual changes).
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/statuses_controller.rb4
-rw-r--r--app/helpers/admin/action_logs_helper.rb6
-rw-r--r--app/javascript/core/admin.js3
-rw-r--r--app/javascript/mastodon/components/status_action_bar.js12
-rw-r--r--app/javascript/mastodon/features/account/components/action_bar.js6
-rw-r--r--app/javascript/mastodon/features/status/index.js4
-rw-r--r--app/javascript/mastodon/features/video/index.js20
-rw-r--r--app/javascript/styles/mastodon/components.scss20
-rw-r--r--app/javascript/styles/mastodon/tables.scss16
-rw-r--r--app/lib/activitypub/activity/follow.rb2
-rw-r--r--app/lib/activitypub/activity/undo.rb6
-rw-r--r--app/models/export.rb10
-rw-r--r--app/models/favourite.rb13
-rw-r--r--app/models/follow.rb5
-rw-r--r--app/models/status.rb72
-rw-r--r--app/models/status_stat.rb17
-rw-r--r--app/serializers/rest/status_serializer.rb3
-rw-r--r--app/services/authorize_follow_service.rb2
-rw-r--r--app/services/resolve_url_service.rb10
-rw-r--r--app/services/search_service.rb2
-rw-r--r--app/views/accounts/_header.html.haml6
-rw-r--r--app/views/admin/reports/_status.html.haml12
-rw-r--r--app/views/settings/exports/show.html.haml14
-rw-r--r--app/views/settings/imports/show.html.haml11
24 files changed, 200 insertions, 76 deletions
diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb
index 382bfc4a2..a69f12084 100644
--- a/app/controllers/admin/statuses_controller.rb
+++ b/app/controllers/admin/statuses_controller.rb
@@ -29,6 +29,10 @@ module Admin
       flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
 
       redirect_to admin_account_statuses_path(@account.id, current_params)
+    rescue ActionController::ParameterMissing
+      flash[:alert] = I18n.t('admin.statuses.no_status_selected')
+
+      redirect_to admin_account_statuses_path(@account.id, current_params)
     end
 
     private
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
index 4c663211e..85bd30304 100644
--- a/app/helpers/admin/action_logs_helper.rb
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -34,7 +34,11 @@ module Admin::ActionLogsHelper
       link_to attributes['domain'], "https://#{attributes['domain']}"
     when 'Status'
       tmp_status = Status.new(attributes)
-      link_to tmp_status.account&.acct || "##{tmp_status.account_id}", TagManager.instance.url_for(tmp_status)
+      if tmp_status.account
+        link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id)
+      else
+        I18n.t('admin.action_logs.deleted_status')
+      end
     end
   end
 
diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js
index 3302454ab..28f27fbc6 100644
--- a/app/javascript/core/admin.js
+++ b/app/javascript/core/admin.js
@@ -1,9 +1,6 @@
 //  This file will be loaded on admin pages, regardless of theme.
 
 import { delegate } from 'rails-ujs';
-import { start } from '../mastodon/common';
-
-start();
 
 function handleDeleteStatus(event) {
   const [data] = event.detail;
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index 0ae21e3f0..c799d4e98 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -32,6 +32,16 @@ const messages = defineMessages({
   embed: { id: 'status.embed', defaultMessage: 'Embed' },
 });
 
+const obfuscatedCount = count => {
+  if (count < 0) {
+    return 0;
+  } else if (count <= 1) {
+    return count;
+  } else {
+    return '1+';
+  }
+};
+
 @injectIntl
 export default class StatusActionBar extends ImmutablePureComponent {
 
@@ -194,7 +204,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
 
     return (
       <div className='status__action-bar'>
-        <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
+        <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
         <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
         <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
         {shareButton}
diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js
index 43b4811e1..bc6f86628 100644
--- a/app/javascript/mastodon/features/account/components/action_bar.js
+++ b/app/javascript/mastodon/features/account/components/action_bar.js
@@ -147,17 +147,17 @@ export default class ActionBar extends React.PureComponent {
 
         <div className='account__action-bar'>
           <div className='account__action-bar-links'>
-            <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}>
+            <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
               <FormattedMessage id='account.posts' defaultMessage='Toots' />
               <strong>{shortNumberFormat(account.get('statuses_count'))}</strong>
             </Link>
 
-            <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}>
+            <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
               <FormattedMessage id='account.follows' defaultMessage='Follows' />
               <strong>{shortNumberFormat(account.get('following_count'))}</strong>
             </Link>
 
-            <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
+            <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
               <FormattedMessage id='account.followers' defaultMessage='Followers' />
               <strong>{shortNumberFormat(account.get('followers_count'))}</strong>
             </Link>
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index 89387ca43..0ffeaa4dc 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -355,7 +355,9 @@ export default class Status extends ImmutablePureComponent {
     if (status && ancestorsIds && ancestorsIds.size > 0) {
       const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1];
 
-      element.scrollIntoView(true);
+      window.requestAnimationFrame(() => {
+        element.scrollIntoView(true);
+      });
       this._scrolledIntoView = true;
     }
   }
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 47a165e16..55ea32acb 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -158,6 +158,9 @@ export default class Video extends React.PureComponent {
     this.setState({ dragging: true });
     this.video.pause();
     this.handleMouseMove(e);
+
+    e.preventDefault();
+    e.stopPropagation();
   }
 
   handleMouseUp = () => {
@@ -174,8 +177,10 @@ export default class Video extends React.PureComponent {
     const { x } = getPointerPosition(this.seek, e);
     const currentTime = Math.floor(this.video.duration * x);
 
-    this.video.currentTime = currentTime;
-    this.setState({ currentTime });
+    if (!isNaN(currentTime)) {
+      this.video.currentTime = currentTime;
+      this.setState({ currentTime });
+    }
   }, 60);
 
   togglePlay = () => {
@@ -281,6 +286,15 @@ export default class Video extends React.PureComponent {
       playerStyle.height = height;
     }
 
+    let preload;
+    if (startTime || fullscreen || dragging) {
+      preload = 'auto';
+    } else if (detailed) {
+      preload = 'metadata';
+    } else {
+      preload = 'none';
+    }
+
     return (
       <div
         role='menuitem'
@@ -296,7 +310,7 @@ export default class Video extends React.PureComponent {
           ref={this.setVideoRef}
           src={src}
           poster={preview}
-          preload={startTime ? 'auto' : 'none'}
+          preload={preload}
           loop
           role='button'
           tabIndex='0'
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 8067b80bb..547bcfd1e 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -921,15 +921,31 @@
   align-items: center;
   display: flex;
   margin-top: 8px;
+
+  &__counter {
+    display: inline-flex;
+    margin-right: 11px;
+    align-items: center;
+
+    .status__action-bar-button {
+      margin-right: 4px;
+    }
+
+    &__label {
+      display: inline-block;
+      width: 14px;
+      font-size: 12px;
+      font-weight: 500;
+      color: $action-button-color;
+    }
+  }
 }
 
 .status__action-bar-button {
-  float: left;
   margin-right: 18px;
 }
 
 .status__action-bar-dropdown {
-  float: left;
   height: 23.15px;
   width: 23.15px;
 }
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index e54c55947..c2206cf55 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -1,9 +1,3 @@
-@keyframes Swag {
-  0% { background-position: 0% 0%; }
-  50% { background-position: 100% 0%; }
-  100% { background-position: 200% 0%; }
-}
-
 .table {
   width: 100%;
   max-width: 100%;
@@ -191,14 +185,12 @@ a.table-action-link {
   .status__content {
     padding-top: 0;
 
+    summary {
+      display: list-item;
+    }
+
     strong {
       font-weight: 700;
-      background: linear-gradient(to right, orange , yellow, green, cyan, blue, violet,orange , yellow, green, cyan, blue, violet);
-      background-size: 200% 100%;
-      -webkit-background-clip: text;
-      background-clip: text;
-      color: transparent;
-      animation: Swag 2s linear 0s infinite;
     }
   }
 }
diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb
index 826dcf18e..c45832648 100644
--- a/app/lib/activitypub/activity/follow.rb
+++ b/app/lib/activitypub/activity/follow.rb
@@ -13,7 +13,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
 
     # Fast-forward repeat follow requests
     if @account.following?(target_account)
-      AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true)
+      AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true, follow_request_uri: @json['id'])
       return
     end
 
diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb
index cbed417c4..64c2be7d9 100644
--- a/app/lib/activitypub/activity/undo.rb
+++ b/app/lib/activitypub/activity/undo.rb
@@ -5,6 +5,8 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
     case @object['type']
     when 'Announce'
       undo_announce
+    when 'Accept'
+      undo_accept
     when 'Follow'
       undo_follow
     when 'Like'
@@ -27,6 +29,10 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
     end
   end
 
+  def undo_accept
+    ::Follow.find_by(target_account: @account, uri: target_uri)&.revoke_request!
+  end
+
   def undo_follow
     target_account = account_from_uri(target_uri)
 
diff --git a/app/models/export.rb b/app/models/export.rb
index f0d5dd255..0eeac0dc0 100644
--- a/app/models/export.rb
+++ b/app/models/export.rb
@@ -24,8 +24,16 @@ class Export
     account.media_attachments.sum(:file_file_size)
   end
 
+  def total_statuses
+    account.statuses_count
+  end
+
   def total_follows
-    account.following.count
+    account.following_count
+  end
+
+  def total_followers
+    account.followers_count
   end
 
   def total_blocks
diff --git a/app/models/favourite.rb b/app/models/favourite.rb
index 0fce82f6f..ce7a6a336 100644
--- a/app/models/favourite.rb
+++ b/app/models/favourite.rb
@@ -32,20 +32,11 @@ class Favourite < ApplicationRecord
   private
 
   def increment_cache_counters
-    if association(:status).loaded?
-      status.update_attribute(:favourites_count, status.favourites_count + 1)
-    else
-      Status.where(id: status_id).update_all('favourites_count = COALESCE(favourites_count, 0) + 1')
-    end
+    status.increment_count!(:favourites_count)
   end
 
   def decrement_cache_counters
     return if association(:status).loaded? && (status.marked_for_destruction? || status.marked_for_mass_destruction?)
-
-    if association(:status).loaded?
-      status.update_attribute(:favourites_count, [status.favourites_count - 1, 0].max)
-    else
-      Status.where(id: status_id).update_all('favourites_count = GREATEST(COALESCE(favourites_count, 0) - 1, 0)')
-    end
+    status.decrement_count!(:favourites_count)
   end
 end
diff --git a/app/models/follow.rb b/app/models/follow.rb
index 3fce14b9a..714f4e898 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -32,6 +32,11 @@ class Follow < ApplicationRecord
     false # Force uri_for to use uri attribute
   end
 
+  def revoke_request!
+    FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, uri: uri)
+    destroy!
+  end
+
   before_validation :set_uri, only: :create
   after_destroy :remove_endorsements
 
diff --git a/app/models/status.rb b/app/models/status.rb
index 398462302..01615c876 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -15,8 +15,6 @@
 #  visibility             :integer          default("public"), not null
 #  spoiler_text           :text             default(""), not null
 #  reply                  :boolean          default(FALSE), not null
-#  favourites_count       :integer          default(0), not null
-#  reblogs_count          :integer          default(0), not null
 #  language               :string
 #  conversation_id        :bigint(8)
 #  local                  :boolean
@@ -28,6 +26,8 @@
 #
 
 class Status < ApplicationRecord
+  self.cache_versioning = false
+
   include Paginable
   include Streamable
   include Cacheable
@@ -62,6 +62,7 @@ class Status < ApplicationRecord
 
   has_one :notification, as: :activity, dependent: :destroy
   has_one :stream_entry, as: :activity, inverse_of: :status
+  has_one :status_stat, inverse_of: :status
 
   validates :uri, uniqueness: true, presence: true, unless: :local?
   validates :text, presence: true, unless: -> { with_media? || reblog? }
@@ -86,7 +87,25 @@ class Status < ApplicationRecord
 
   scope :not_local_only, -> { where(local_only: [false, nil]) }
 
-  cache_associated :account, :application, :media_attachments, :conversation, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, :conversation, mentions: :account], thread: :account
+  cache_associated :account,
+                   :application,
+                   :media_attachments,
+                   :conversation,
+                   :status_stat,
+                   :tags,
+                   :stream_entry,
+                   mentions: :account,
+                   reblog: [
+                     :account,
+                     :application,
+                     :stream_entry,
+                     :tags,
+                     :media_attachments,
+                     :conversation,
+                     :status_stat,
+                     mentions: :account,
+                   ],
+                   thread: :account
 
   delegate :domain, to: :account, prefix: true
 
@@ -180,6 +199,26 @@ class Status < ApplicationRecord
     @marked_for_mass_destruction
   end
 
+  def replies_count
+    status_stat&.replies_count || 0
+  end
+
+  def reblogs_count
+    status_stat&.reblogs_count || 0
+  end
+
+  def favourites_count
+    status_stat&.favourites_count || 0
+  end
+
+  def increment_count!(key)
+    update_status_stat!(key => public_send(key) + 1)
+  end
+
+  def decrement_count!(key)
+    update_status_stat!(key => [public_send(key) - 1, 0].max)
+  end
+
   after_create  :increment_counter_caches
   after_destroy :decrement_counter_caches
 
@@ -197,6 +236,10 @@ class Status < ApplicationRecord
   before_validation :set_local
 
   class << self
+    def cache_ids
+      left_outer_joins(:status_stat).select('statuses.id, greatest(statuses.updated_at, status_stats.updated_at) AS updated_at')
+    end
+
     def in_chosen_languages(account)
       where(language: nil).or where(language: account.chosen_languages)
     end
@@ -372,6 +415,11 @@ class Status < ApplicationRecord
 
   private
 
+  def update_status_stat!(attrs)
+    record = status_stat || build_status_stat
+    record.update(attrs)
+  end
+
   def store_uri
     update_attribute(:uri, ActivityPub::TagManager.instance.uri_for(self)) if uri.nil?
   end
@@ -434,13 +482,8 @@ class Status < ApplicationRecord
       Account.where(id: account_id).update_all('statuses_count = COALESCE(statuses_count, 0) + 1')
     end
 
-    return unless reblog?
-
-    if association(:reblog).loaded?
-      reblog.update_attribute(:reblogs_count, reblog.reblogs_count + 1)
-    else
-      Status.where(id: reblog_of_id).update_all('reblogs_count = COALESCE(reblogs_count, 0) + 1')
-    end
+    reblog.increment_count!(:reblogs_count) if reblog?
+    thread.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
   end
 
   def decrement_counter_caches
@@ -452,12 +495,7 @@ class Status < ApplicationRecord
       Account.where(id: account_id).update_all('statuses_count = GREATEST(COALESCE(statuses_count, 0) - 1, 0)')
     end
 
-    return unless reblog?
-
-    if association(:reblog).loaded?
-      reblog.update_attribute(:reblogs_count, [reblog.reblogs_count - 1, 0].max)
-    else
-      Status.where(id: reblog_of_id).update_all('reblogs_count = GREATEST(COALESCE(reblogs_count, 0) - 1, 0)')
-    end
+    reblog.decrement_count!(:reblogs_count) if reblog?
+    thread.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
   end
 end
diff --git a/app/models/status_stat.rb b/app/models/status_stat.rb
new file mode 100644
index 000000000..9d358776b
--- /dev/null
+++ b/app/models/status_stat.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: status_stats
+#
+#  id               :bigint(8)        not null, primary key
+#  status_id        :bigint(8)        not null
+#  replies_count    :bigint(8)        default(0), not null
+#  reblogs_count    :bigint(8)        default(0), not null
+#  favourites_count :bigint(8)        default(0), not null
+#  created_at       :datetime         not null
+#  updated_at       :datetime         not null
+#
+
+class StatusStat < ApplicationRecord
+  belongs_to :status, inverse_of: :status_stat
+end
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index c8fffcf68..89776b1fc 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -3,7 +3,8 @@
 class REST::StatusSerializer < ActiveModel::Serializer
   attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
              :sensitive, :spoiler_text, :visibility, :language,
-             :uri, :content, :url, :reblogs_count, :favourites_count
+             :uri, :content, :url, :replies_count, :reblogs_count,
+             :favourites_count
 
   attribute :favourited, if: :current_user?
   attribute :reblogged, if: :current_user?
diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb
index f47d488f1..1674239df 100644
--- a/app/services/authorize_follow_service.rb
+++ b/app/services/authorize_follow_service.rb
@@ -3,7 +3,7 @@
 class AuthorizeFollowService < BaseService
   def call(source_account, target_account, **options)
     if options[:skip_follow_request]
-      follow_request = FollowRequest.new(account: source_account, target_account: target_account)
+      follow_request = FollowRequest.new(account: source_account, target_account: target_account, uri: options[:follow_request_uri])
     else
       follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
       follow_request.authorize!
diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb
index a068c1ed8..1db1917e2 100644
--- a/app/services/resolve_url_service.rb
+++ b/app/services/resolve_url_service.rb
@@ -2,11 +2,13 @@
 
 class ResolveURLService < BaseService
   include JsonLdHelper
+  include Authorization
 
   attr_reader :url
 
-  def call(url)
+  def call(url, on_behalf_of: nil)
     @url = url
+    @on_behalf_of = on_behalf_of
 
     return process_local_url if local_url?
 
@@ -84,6 +86,10 @@ class ResolveURLService < BaseService
 
   def check_local_status(status)
     return if status.nil?
-    status if status.public_visibility? || status.unlisted_visibility?
+    authorize_with @on_behalf_of, status, :show?
+    status
+  rescue Mastodon::NotPermittedError
+    # Do not disclose the existence of status the user is not authorized to see
+    nil
   end
 end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 5bb395942..cc1fcb52f 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -53,7 +53,7 @@ class SearchService < BaseService
   end
 
   def url_resource
-    @_url_resource ||= ResolveURLService.new.call(query)
+    @_url_resource ||= ResolveURLService.new.call(query, on_behalf_of: @account)
   end
 
   def url_resource_symbol
diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml
index d3b9893c4..caf03bd7c 100644
--- a/app/views/accounts/_header.html.haml
+++ b/app/views/accounts/_header.html.haml
@@ -14,17 +14,17 @@
       .public-account-header__tabs__tabs
         .details-counters
           .counter{ class: active_nav_class(short_account_url(account)) }
-            = link_to short_account_url(account), class: 'u-url u-uid' do
+            = link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do
               %span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true
               %span.counter-label= t('accounts.posts')
 
           .counter{ class: active_nav_class(account_following_index_url(account)) }
-            = link_to account_following_index_url(account) do
+            = link_to account_following_index_url(account), title: number_with_delimiter(account.following_count) do
               %span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true
               %span.counter-label= t('accounts.following')
 
           .counter{ class: active_nav_class(account_followers_url(account)) }
-            = link_to account_followers_url(account) do
+            = link_to account_followers_url(account), title: number_with_delimiter(account.followers_count) do
               %span.counter-number= number_to_human account.followers_count, strip_insignificant_zeros: true
               %span.counter-label= t('accounts.followers')
         .spacer
diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml
index 5e174f312..5b410ec84 100644
--- a/app/views/admin/reports/_status.html.haml
+++ b/app/views/admin/reports/_status.html.haml
@@ -3,11 +3,13 @@
     = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
   .batch-table__row__content
     .status__content><
-      - unless status.proper.spoiler_text.blank?
-        %p><
-          %strong> Content warning: #{Formatter.instance.format_spoiler(status.proper)}
-
-      = Formatter.instance.format(status.proper, custom_emojify: true)
+      - if status.proper.spoiler_text.blank?
+        = Formatter.instance.format(status.proper, custom_emojify: true)
+      - else
+        %details<
+          %summary><
+            %strong> Content warning: #{Formatter.instance.format_spoiler(status.proper)}
+          = Formatter.instance.format(status.proper, custom_emojify: true)
 
     - unless status.proper.media_attachments.empty?
       - if status.proper.media_attachments.first.video?
diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml
index 30cd26914..ef2d2b894 100644
--- a/app/views/settings/exports/show.html.haml
+++ b/app/views/settings/exports/show.html.haml
@@ -9,16 +9,24 @@
         %td= number_to_human_size @export.total_storage
         %td
       %tr
+        %th= t('accounts.statuses')
+        %td= number_with_delimiter @export.total_statuses
+        %td
+      %tr
         %th= t('exports.follows')
-        %td= number_to_human @export.total_follows
+        %td= number_with_delimiter @export.total_follows
         %td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv)
       %tr
+        %th= t('accounts.followers')
+        %td= number_with_delimiter @export.total_followers
+        %td
+      %tr
         %th= t('exports.blocks')
-        %td= number_to_human @export.total_blocks
+        %td= number_with_delimiter @export.total_blocks
         %td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv)
       %tr
         %th= t('exports.mutes')
-        %td= number_to_human @export.total_mutes
+        %td= number_with_delimiter @export.total_mutes
         %td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv)
 
 %p.muted-hint= t('exports.archive_takeout.hint_html')
diff --git a/app/views/settings/imports/show.html.haml b/app/views/settings/imports/show.html.haml
index 991dd4e94..2b43cb134 100644
--- a/app/views/settings/imports/show.html.haml
+++ b/app/views/settings/imports/show.html.haml
@@ -1,11 +1,14 @@
 - content_for :page_title do
   = t('settings.import')
 
-%p.hint= t('imports.preface')
-
 = simple_form_for @import, url: settings_import_path do |f|
-  = f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
-  = f.input :data, wrapper: :with_label, hint: t('simple_form.hints.imports.data')
+  %p.hint= t('imports.preface')
+
+  .field-group
+    = f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
+
+  .field-group
+    = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data')
 
   .actions
     = f.button :button, t('imports.upload'), type: :submit