about summary refs log tree commit diff
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2020-01-04 22:54:06 +0100
committerThibaut Girka <thib@sitedethib.com>2020-01-04 23:04:42 +0100
commit01eaeab56df4da4c697b1096f40a400cc9e2b8e8 (patch)
tree6288ee106b4615cacd98362f9fd268317a9cdeff
parent22daf24600d8e99e4569740ee5836d25c70c1e8b (diff)
parent2ecc7802caf4d272191a7fd582fc97996f750827 (diff)
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts:
- `app/controllers/application_controller.rb`:
  Conflict due to theming system.
- `app/controllers/oauth/authorizations_controller.rb`:
  Conflict due to theming system.
-rw-r--r--.circleci/config.yml26
-rw-r--r--Dockerfile2
-rw-r--r--Gemfile14
-rw-r--r--Gemfile.lock46
-rw-r--r--app/controllers/admin/custom_emojis_controller.rb4
-rw-r--r--app/controllers/api/v1/accounts/follower_accounts_controller.rb6
-rw-r--r--app/controllers/api/v1/accounts/following_accounts_controller.rb6
-rw-r--r--app/controllers/api/v1/media_controller.rb3
-rw-r--r--app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb4
-rw-r--r--app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb4
-rw-r--r--app/controllers/application_controller.rb10
-rw-r--r--app/controllers/auth/registrations_controller.rb5
-rw-r--r--app/controllers/concerns/obfuscate_filename.rb16
-rw-r--r--app/controllers/filters_controller.rb3
-rw-r--r--app/controllers/follower_accounts_controller.rb7
-rw-r--r--app/controllers/following_accounts_controller.rb7
-rw-r--r--app/controllers/oauth/authorizations_controller.rb5
-rw-r--r--app/controllers/settings/base_controller.rb5
-rw-r--r--app/controllers/settings/profiles_controller.rb5
-rw-r--r--app/controllers/well_known/host_meta_controller.rb6
-rw-r--r--app/helpers/accounts_helper.rb2
-rw-r--r--app/javascript/mastodon/actions/statuses.js13
-rw-r--r--app/javascript/mastodon/components/scrollable_list.js7
-rw-r--r--app/javascript/mastodon/components/status.js48
-rw-r--r--app/javascript/mastodon/components/status_content.js25
-rw-r--r--app/javascript/mastodon/containers/status_container.js5
-rw-r--r--app/javascript/mastodon/reducers/statuses.js3
-rw-r--r--app/javascript/mastodon/utils/numbers.js8
-rw-r--r--app/javascript/styles/mastodon/components.scss4
-rw-r--r--app/lib/search_query_transformer.rb2
-rw-r--r--app/middleware/handle_bad_encoding_middleware.rb18
-rw-r--r--app/models/account.rb6
-rw-r--r--app/models/concerns/attachmentable.rb11
-rw-r--r--app/models/media_attachment.rb3
-rw-r--r--app/serializers/rest/account_serializer.rb4
-rw-r--r--app/services/backup_service.rb2
-rw-r--r--app/services/process_mentions_service.rb11
-rw-r--r--app/services/resolve_url_service.rb18
-rw-r--r--app/views/admin/tags/_tag.html.haml5
-rw-r--r--app/views/admin/tags/index.html.haml15
-rw-r--r--app/views/settings/preferences/appearance/show.html.haml4
-rw-r--r--app/views/statuses/embed.html.haml5
-rw-r--r--app/workers/refollow_worker.rb9
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/doorkeeper.rb13
-rw-r--r--config/initializers/paperclip.rb2
-rw-r--r--config/initializers/rack_attack.rb3
-rw-r--r--config/locales/en.yml4
-rw-r--r--db/migrate/20170711225116_fix_null_booleans.rb20
-rw-r--r--db/migrate/20171010025614_change_accounts_nonnullable_in_account_moderation_notes.rb6
-rw-r--r--db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb4
-rw-r--r--db/migrate/20180310000000_change_columns_in_notifications_nonnullable.rb10
-rw-r--r--package.json16
-rw-r--r--spec/controllers/api/proofs_controller_spec.rb5
-rw-r--r--spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb27
-rw-r--r--spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb27
-rw-r--r--spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb20
-rw-r--r--spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb20
-rw-r--r--spec/controllers/concerns/obfuscate_filename_spec.rb30
-rw-r--r--spec/controllers/follower_accounts_controller_spec.rb12
-rw-r--r--spec/controllers/following_accounts_controller_spec.rb12
-rw-r--r--spec/features/log_in_spec.rb36
-rw-r--r--spec/features/profile_spec.rb53
-rw-r--r--spec/middleware/handle_bad_encoding_middleware_spec.rb21
-rw-r--r--spec/models/account_spec.rb1
-rw-r--r--spec/models/media_attachment_spec.rb18
-rw-r--r--spec/services/process_mentions_service_spec.rb46
-rw-r--r--spec/support/examples/models/concerns/account_avatar.rb20
-rw-r--r--spec/support/examples/models/concerns/account_header.rb23
-rw-r--r--spec/support/stories/profile_stories.rb45
-rw-r--r--spec/workers/refollow_worker_spec.rb30
-rw-r--r--yarn.lock146
72 files changed, 708 insertions, 376 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
index ff8eb4859..83a2088d7 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -3,7 +3,7 @@ version: 2
 aliases:
   - &defaults
     docker:
-      - image: circleci/ruby:2.6-stretch-node
+      - image: circleci/ruby:2.6-buster-node
         environment: &ruby_environment
           BUNDLE_APP_CONFIG: ./.bundle/
           DB_HOST: localhost
@@ -39,7 +39,6 @@ aliases:
     steps:
       - checkout
       - *attach_workspace
-
       - restore_cache:
           keys:
             - v1-node-dependencies-{{ checksum "yarn.lock" }}
@@ -49,7 +48,6 @@ aliases:
           key: v1-node-dependencies-{{ checksum "yarn.lock" }}
           paths:
             - ./node_modules/
-
       - *persist_to_workspace
 
   - &install_system_dependencies
@@ -58,13 +56,17 @@ aliases:
         command: |
           sudo apt-get update
           sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
+          
+          ## TODO: FIX THESE BUSTER DEPENDANCES
+          sudo wget http://ftp.au.debian.org/debian/pool/main/i/icu/libicu57_57.1-6+deb9u3_amd64.deb
+          sudo dpkg -i libicu57_57.1-6+deb9u3_amd64.deb
+          sudo wget http://ftp.au.debian.org/debian/pool/main/p/protobuf/libprotobuf10_3.0.0-9_amd64.deb
+          sudo dpkg -i libprotobuf10_3.0.0-9_amd64.deb
 
   - &install_ruby_dependencies
       steps:
         - *attach_workspace
-
         - *install_system_dependencies
-
         - run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
         - *restore_ruby_dependencies
         - run: bundle install --clean --jobs 16 --path ./vendor/bundle/ --retry 3 --with pam_authentication --without development production && bundle clean
@@ -82,10 +84,8 @@ aliases:
   - &test_steps
       steps:
         - *attach_workspace
-
         - *install_system_dependencies
         - run: sudo apt-get install -y ffmpeg
-
         - run:
             name: Prepare Tests
             command: ./bin/rails parallel:create parallel:load_schema parallel:prepare
@@ -105,14 +105,14 @@ jobs:
   install-ruby2.5:
     <<: *defaults
     docker:
-      - image: circleci/ruby:2.5-stretch-node
+      - image: circleci/ruby:2.5-buster-node
         environment: *ruby_environment
     <<: *install_ruby_dependencies
 
   install-ruby2.4:
     <<: *defaults
     docker:
-      - image: circleci/ruby:2.4-stretch-node
+      - image: circleci/ruby:2.4-buster-node
         environment: *ruby_environment
     <<: *install_ruby_dependencies
 
@@ -131,7 +131,7 @@ jobs:
   test-ruby2.6:
     <<: *defaults
     docker:
-      - image: circleci/ruby:2.6-stretch-node
+      - image: circleci/ruby:2.6-buster-node
         environment: *ruby_environment
       - image: circleci/postgres:10.6-alpine
         environment:
@@ -142,7 +142,7 @@ jobs:
   test-ruby2.5:
     <<: *defaults
     docker:
-      - image: circleci/ruby:2.5-stretch-node
+      - image: circleci/ruby:2.5-buster-node
         environment: *ruby_environment
       - image: circleci/postgres:10.6-alpine
         environment:
@@ -153,7 +153,7 @@ jobs:
   test-ruby2.4:
     <<: *defaults
     docker:
-      - image: circleci/ruby:2.4-stretch-node
+      - image: circleci/ruby:2.4-buster-node
         environment: *ruby_environment
       - image: circleci/postgres:10.6-alpine
         environment:
@@ -164,7 +164,7 @@ jobs:
   test-webui:
     <<: *defaults
     docker:
-      - image: circleci/node:12.9-stretch
+      - image: circleci/node:12-buster
     steps:
       - *attach_workspace
       - run: ./bin/retry yarn test:jest
diff --git a/Dockerfile b/Dockerfile
index 117727d08..cc75bd6be 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@ FROM ubuntu:18.04 as build-dep
 SHELL ["bash", "-c"]
 
 # Install Node v12 (LTS)
-ENV NODE_VER="12.13.1"  
+ENV NODE_VER="12.14.0"  
 RUN	echo "Etc/UTC" > /etc/localtime && \
 	apt update && \
 	apt -y install wget python && \
diff --git a/Gemfile b/Gemfile
index bb4e65fdc..a0ad8e464 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,11 +7,11 @@ gem 'pkg-config', '~> 1.4'
 
 gem 'puma', '~> 4.3'
 gem 'rails', '~> 5.2.4'
-gem 'sprockets', '~> 3.7'
+gem 'sprockets', '~> 3.7.2'
 gem 'thor', '~> 0.20'
 
 gem 'hamlit-rails', '~> 0.2'
-gem 'pg', '~> 1.1'
+gem 'pg', '~> 1.2'
 gem 'makara', '~> 0.4'
 gem 'pghero', '~> 2.4'
 gem 'dotenv-rails', '~> 2.7'
@@ -31,7 +31,7 @@ gem 'browser'
 gem 'charlock_holmes', '~> 0.7.7'
 gem 'iso-639'
 gem 'chewy', '~> 5.1'
-gem 'cld3', '~> 3.2.4'
+gem 'cld3', '~> 3.2.6'
 gem 'devise', '~> 4.7'
 gem 'devise-two-factor', '~> 3.1'
 
@@ -50,7 +50,7 @@ gem 'fast_blank', '~> 1.0'
 gem 'fastimage'
 gem 'goldfinger', '~> 2.1'
 gem 'hiredis', '~> 0.6'
-gem 'redis-namespace', '~> 1.5'
+gem 'redis-namespace', '~> 1.7'
 gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
 gem 'html2text'
 gem 'htmlentities', '~> 4.3'
@@ -61,7 +61,7 @@ gem 'httplog', '~> 1.3'
 gem 'idn-ruby', require: 'idn'
 gem 'kaminari', '~> 1.1'
 gem 'link_header', '~> 0.0'
-gem 'mime-types', '~> 3.3', require: 'mime/types/columnar'
+gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
 gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
 gem 'nokogiri', '~> 1.10'
 gem 'nsa', '~> 0.2'
@@ -120,8 +120,8 @@ end
 group :test do
   gem 'capybara', '~> 3.29'
   gem 'climate_control', '~> 0.2'
-  gem 'faker', '~> 2.9'
-  gem 'microformats', '~> 4.1'
+  gem 'faker', '~> 2.10'
+  gem 'microformats', '~> 4.2'
   gem 'rails-controller-testing', '~> 1.0'
   gem 'rspec-sidekiq', '~> 3.0'
   gem 'simplecov', '~> 0.17', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index cfa4201fd..fec95d27f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -185,10 +185,10 @@ GEM
     crack (0.4.3)
       safe_yaml (~> 1.0.0)
     crass (1.0.5)
-    css_parser (1.7.0)
+    css_parser (1.7.1)
       addressable
     debug_inspector (0.0.3)
-    derailed_benchmarks (1.4.2)
+    derailed_benchmarks (1.4.3)
       benchmark-ips (~> 2)
       get_process_mem (~> 0)
       heapy (~> 0)
@@ -240,9 +240,9 @@ GEM
       tzinfo
     excon (0.71.0)
     fabrication (2.21.0)
-    faker (2.9.0)
+    faker (2.10.0)
       i18n (>= 1.6, < 1.8)
-    faraday (0.15.4)
+    faraday (1.0.0)
       multipart-post (>= 1.2, < 3)
     fast_blank (1.0.0)
     fastimage (2.1.7)
@@ -275,8 +275,8 @@ GEM
       http (~> 3.0)
       nokogiri (~> 1.8)
       oj (~> 3.0)
-    hamlit (2.9.3)
-      temple (>= 0.8.0)
+    hamlit (2.11.0)
+      temple (>= 0.8.2)
       thor
       tilt
     hamlit-rails (0.2.3)
@@ -374,9 +374,9 @@ GEM
     microformats (4.1.0)
       json (~> 2.1)
       nokogiri (~> 1.8, >= 1.8.3)
-    mime-types (3.3)
+    mime-types (3.3.1)
       mime-types-data (~> 3.2015)
-    mime-types-data (3.2019.0904)
+    mime-types-data (3.2019.1009)
     mimemagic (0.3.3)
     mini_mime (1.0.2)
     mini_portile2 (2.4.0)
@@ -434,7 +434,7 @@ GEM
     pastel (0.7.3)
       equatable (~> 0.6)
       tty-color (~> 0.5)
-    pg (1.1.4)
+    pg (1.2.0)
     pghero (2.4.1)
       activerecord (>= 5)
     pkg-config (1.4.0)
@@ -454,7 +454,7 @@ GEM
       pry (~> 0.10)
     pry-rails (0.3.9)
       pry (>= 0.10.4)
-    public_suffix (4.0.1)
+    public_suffix (4.0.2)
     puma (4.3.1)
       nio4r (~> 2.0)
     pundit (2.1.0)
@@ -463,7 +463,7 @@ GEM
     rack (2.0.8)
     rack-attack (6.2.2)
       rack (>= 1.0, < 3)
-    rack-cors (1.1.0)
+    rack-cors (1.1.1)
       rack (>= 2.0.0)
     rack-protection (2.0.7)
       rack
@@ -520,7 +520,7 @@ GEM
     redis-activesupport (5.0.4)
       activesupport (>= 3, < 6)
       redis-store (>= 1.3, < 2)
-    redis-namespace (1.6.0)
+    redis-namespace (1.7.0)
       redis (>= 3.0.4)
     redis-rack (2.0.4)
       rack (>= 1.5, < 3)
@@ -532,7 +532,7 @@ GEM
     redis-store (1.5.0)
       redis (>= 2.2, < 5)
     regexp_parser (1.6.0)
-    request_store (1.4.1)
+    request_store (1.5.0)
       rack (>= 1.4)
     responders (3.0.0)
       actionpack (>= 5.0)
@@ -618,21 +618,21 @@ GEM
     sshkit (1.20.0)
       net-scp (>= 1.1.2)
       net-ssh (>= 2.8.0)
-    stackprof (0.2.14)
+    stackprof (0.2.15)
     statsd-ruby (1.4.0)
     stoplight (2.2.0)
     streamio-ffmpeg (3.0.2)
       multi_json (~> 1.8)
     strong_migrations (0.5.1)
       activerecord (>= 5)
-    temple (0.8.1)
+    temple (0.8.2)
     terminal-table (1.8.0)
       unicode-display_width (~> 1.1, >= 1.1.1)
     terrapin (0.6.0)
       climate_control (>= 0.0.3, < 1.0)
     thor (0.20.3)
     thread_safe (0.3.6)
-    tilt (2.0.9)
+    tilt (2.0.10)
     tty-color (0.5.0)
     tty-command (0.9.0)
       pastel (~> 0.7.0)
@@ -654,7 +654,7 @@ GEM
       tzinfo (>= 1.0.0)
     unf (0.1.4)
       unf_ext
-    unf_ext (0.0.7.5)
+    unf_ext (0.0.7.6)
     unicode-display_width (1.6.0)
     uniform_notifier (1.12.1)
     warden (1.2.8)
@@ -713,7 +713,7 @@ DEPENDENCIES
   doorkeeper (~> 5.2)
   dotenv-rails (~> 2.7)
   fabrication (~> 2.21)
-  faker (~> 2.9)
+  faker (~> 2.10)
   fast_blank (~> 1.0)
   fastimage
   fog-core (<= 2.1.0)
@@ -759,7 +759,7 @@ DEPENDENCIES
   parallel (~> 1.19)
   parallel_tests (~> 2.30)
   parslet
-  pg (~> 1.1)
+  pg (~> 1.2)
   pghero (~> 2.4)
   pkg-config (~> 1.4)
   posix-spawn!
@@ -778,7 +778,7 @@ DEPENDENCIES
   rdf-normalize (~> 0.3)
   redcarpet (~> 3.4)
   redis (~> 4.1)
-  redis-namespace (~> 1.5)
+  redis-namespace (~> 1.7)
   redis-rails (~> 5.0)
   rqrcode (~> 0.10)
   rspec-rails (~> 3.9)
@@ -808,9 +808,3 @@ DEPENDENCIES
   webmock (~> 3.7)
   webpacker (~> 4.2)
   webpush
-
-RUBY VERSION
-   ruby 2.6.5p114
-
-BUNDLED WITH
-   1.17.3
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb
index 2af90f051..a446465c9 100644
--- a/app/controllers/admin/custom_emojis_controller.rb
+++ b/app/controllers/admin/custom_emojis_controller.rb
@@ -2,10 +2,6 @@
 
 module Admin
   class CustomEmojisController < BaseController
-    include ObfuscateFilename
-
-    obfuscate_filename [:custom_emoji, :image]
-
     def index
       authorize :custom_emoji, :index?
 
diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
index 2dabb8398..e360b8a92 100644
--- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
@@ -21,11 +21,13 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
   def load_accounts
     return [] if hide_results?
 
-    default_accounts.merge(paginated_follows).to_a
+    scope = default_accounts
+    scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
+    scope.merge(paginated_follows).to_a
   end
 
   def hide_results?
-    (@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
+    (@account.user_hides_network? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
   end
 
   def default_accounts
diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb
index 44e89804b..a405b365f 100644
--- a/app/controllers/api/v1/accounts/following_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb
@@ -21,11 +21,13 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
   def load_accounts
     return [] if hide_results?
 
-    default_accounts.merge(paginated_follows).to_a
+    scope = default_accounts
+    scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
+    scope.merge(paginated_follows).to_a
   end
 
   def hide_results?
-    (@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
+    (@account.user_hides_network? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
   end
 
   def default_accounts
diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb
index aaa93b615..81825db15 100644
--- a/app/controllers/api/v1/media_controller.rb
+++ b/app/controllers/api/v1/media_controller.rb
@@ -4,9 +4,6 @@ class Api::V1::MediaController < Api::BaseController
   before_action -> { doorkeeper_authorize! :write, :'write:media' }
   before_action :require_user!
 
-  include ObfuscateFilename
-  obfuscate_filename :file
-
   respond_to :json
 
   def create
diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
index 657e57831..99eff360e 100644
--- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
@@ -17,7 +17,9 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
   private
 
   def load_accounts
-    default_accounts.merge(paginated_favourites).to_a
+    scope = default_accounts
+    scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
+    scope.merge(paginated_favourites).to_a
   end
 
   def default_accounts
diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
index 6851099f6..cc285ad23 100644
--- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
@@ -17,7 +17,9 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
   private
 
   def load_accounts
-    default_accounts.merge(paginated_statuses).to_a
+    scope = default_accounts
+    scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
+    scope.merge(paginated_statuses).to_a
   end
 
   def default_accounts
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index f1a4f0d02..c882d40ab 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -25,6 +25,7 @@ class ApplicationController < ActionController::Base
   rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
   rescue_from ActionController::UnknownFormat, with: :not_acceptable
   rescue_from ActionController::ParameterMissing, with: :bad_request
+  rescue_from Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
   rescue_from ActiveRecord::RecordNotFound, with: :not_found
   rescue_from Mastodon::NotPermittedError, with: :forbidden
   rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
@@ -211,7 +212,12 @@ class ApplicationController < ActionController::Base
   end
 
   def respond_with_error(code)
-    use_pack 'error'
-    render "errors/#{code}", layout: 'error', status: code, formats: [:html]
+    respond_to do |format|
+      format.any do
+        use_pack 'error'
+        render "errors/#{code}", layout: 'error', status: code, formats: [:html]
+      end
+      format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code }
+    end
   end
 end
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index 068375843..a9d075a45 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -11,6 +11,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
   before_action :set_instance_presenter, only: [:new, :create, :update]
   before_action :set_body_classes, only: [:new, :create, :edit, :update]
   before_action :require_not_suspended!, only: [:update]
+  before_action :set_cache_headers, only: [:edit, :update]
 
   skip_before_action :require_functional!, only: [:edit, :update]
 
@@ -114,4 +115,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
   def require_not_suspended!
     forbidden if current_account.suspended?
   end
+
+  def set_cache_headers
+    response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
+  end
 end
diff --git a/app/controllers/concerns/obfuscate_filename.rb b/app/controllers/concerns/obfuscate_filename.rb
deleted file mode 100644
index 22736ec3a..000000000
--- a/app/controllers/concerns/obfuscate_filename.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module ObfuscateFilename
-  extend ActiveSupport::Concern
-
-  class_methods do
-    def obfuscate_filename(path)
-      before_action do
-        file = params.dig(*path)
-        next if file.nil?
-
-        file.original_filename = SecureRandom.hex(8) + File.extname(file.original_filename)
-      end
-    end
-  end
-end
diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb
index f1e110d87..76be03e53 100644
--- a/app/controllers/filters_controller.rb
+++ b/app/controllers/filters_controller.rb
@@ -1,10 +1,9 @@
 # frozen_string_literal: true
 
 class FiltersController < ApplicationController
-  include Authorization
-
   layout 'admin'
 
+  before_action :authenticate_user!
   before_action :set_filters, only: :index
   before_action :set_filter, only: [:edit, :update, :destroy]
   before_action :set_pack
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index df46f5f72..a5dfffd6d 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -19,7 +19,6 @@ class FollowerAccountsController < ApplicationController
         next if @account.user_hides_network?
 
         follows
-        @relationships = AccountRelationshipsPresenter.new(follows.map(&:account_id), current_user.account_id) if user_signed_in?
       end
 
       format.json do
@@ -38,7 +37,11 @@ class FollowerAccountsController < ApplicationController
   private
 
   def follows
-    @follows ||= Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
+    return @follows if defined?(@follows)
+
+    scope = Follow.where(target_account: @account)
+    scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) if user_signed_in?
+    @follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
   end
 
   def page_requested?
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 8cab67ff5..ff23d97f9 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -19,7 +19,6 @@ class FollowingAccountsController < ApplicationController
         next if @account.user_hides_network?
 
         follows
-        @relationships = AccountRelationshipsPresenter.new(follows.map(&:target_account_id), current_user.account_id) if user_signed_in?
       end
 
       format.json do
@@ -38,7 +37,11 @@ class FollowingAccountsController < ApplicationController
   private
 
   def follows
-    @follows ||= Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
+    return @follows if defined?(@follows)
+
+    scope = Follow.where(account: @account)
+    scope = scope.where.not(target_account_id: current_account.excluded_from_timeline_account_ids) if user_signed_in?
+    @follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
   end
 
   def page_requested?
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index f6f5d1ecc..137346ed0 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -6,6 +6,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
   before_action :store_current_location
   before_action :authenticate_resource_owner!
   before_action :set_pack
+  before_action :set_cache_headers
 
   include Localized
 
@@ -32,4 +33,8 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
   def truthy_param?(key)
     ActiveModel::Type::Boolean.new.cast(params[key])
   end
+
+  def set_cache_headers
+    response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
+  end
 end
diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb
index 8c394a6d3..b97603af6 100644
--- a/app/controllers/settings/base_controller.rb
+++ b/app/controllers/settings/base_controller.rb
@@ -3,6 +3,7 @@
 class Settings::BaseController < ApplicationController
   before_action :set_pack
   before_action :set_body_classes
+  before_action :set_cache_headers
 
   private
 
@@ -13,4 +14,8 @@ class Settings::BaseController < ApplicationController
   def set_body_classes
     @body_classes = 'admin'
   end
+
+  def set_cache_headers
+    response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
+  end
 end
diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb
index 8b640cdca..19a7ce157 100644
--- a/app/controllers/settings/profiles_controller.rb
+++ b/app/controllers/settings/profiles_controller.rb
@@ -1,16 +1,11 @@
 # frozen_string_literal: true
 
 class Settings::ProfilesController < Settings::BaseController
-  include ObfuscateFilename
-
   layout 'admin'
 
   before_action :authenticate_user!
   before_action :set_account
 
-  obfuscate_filename [:account, :avatar]
-  obfuscate_filename [:account, :header]
-
   def show
     @account.build_fields
   end
diff --git a/app/controllers/well_known/host_meta_controller.rb b/app/controllers/well_known/host_meta_controller.rb
index 2e9298c4a..2fd6bc7cc 100644
--- a/app/controllers/well_known/host_meta_controller.rb
+++ b/app/controllers/well_known/host_meta_controller.rb
@@ -8,12 +8,8 @@ module WellKnown
 
     def show
       @webfinger_template = "#{webfinger_url}?resource={uri}"
-
-      respond_to do |format|
-        format.xml { render content_type: 'application/xrd+xml' }
-      end
-
       expires_in 3.days, public: true
+      render content_type: 'application/xrd+xml', formats: [:xml]
     end
   end
 end
diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb
index 7fcc4e816..986fd1805 100644
--- a/app/helpers/accounts_helper.rb
+++ b/app/helpers/accounts_helper.rb
@@ -13,7 +13,7 @@ module AccountsHelper
     if account.local?
       "@#{account.acct}@#{Rails.configuration.x.local_domain}"
     else
-      "@#{account.acct}"
+      "@#{account.pretty_acct}"
     end
   end
 
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 06a19afc3..5640201c6 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -26,8 +26,9 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
 export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
 export const STATUS_UNMUTE_FAIL    = 'STATUS_UNMUTE_FAIL';
 
-export const STATUS_REVEAL = 'STATUS_REVEAL';
-export const STATUS_HIDE   = 'STATUS_HIDE';
+export const STATUS_REVEAL   = 'STATUS_REVEAL';
+export const STATUS_HIDE     = 'STATUS_HIDE';
+export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
 
 export const REDRAFT = 'REDRAFT';
 
@@ -320,3 +321,11 @@ export function revealStatus(ids) {
     ids,
   };
 };
+
+export function toggleStatusCollapse(id, isCollapsed) {
+  return {
+    type: STATUS_COLLAPSE,
+    id,
+    isCollapsed,
+  };
+}
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index 421756803..47a87b149 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -208,10 +208,13 @@ export default class ScrollableList extends PureComponent {
   }
 
   attachIntersectionObserver () {
-    this.intersectionObserverWrapper.connect({
+    let nodeOptions = {
       root: this.node,
       rootMargin: '300% 0px',
-    });
+    };
+
+    this.intersectionObserverWrapper
+      .connect(this.props.bindToDocument ? {} : nodeOptions);
   }
 
   detachIntersectionObserver () {
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index e120278a0..0dc00cb98 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -76,6 +76,7 @@ class Status extends ImmutablePureComponent {
     onEmbed: PropTypes.func,
     onHeightChange: PropTypes.func,
     onToggleHidden: PropTypes.func,
+    onToggleCollapsed: PropTypes.func,
     muted: PropTypes.bool,
     hidden: PropTypes.bool,
     unread: PropTypes.bool,
@@ -102,19 +103,6 @@ class Status extends ImmutablePureComponent {
     statusId: undefined,
   };
 
-  // Track height changes we know about to compensate scrolling
-  componentDidMount () {
-    this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
-  }
-
-  getSnapshotBeforeUpdate () {
-    if (this.props.getScrollPosition) {
-      return this.props.getScrollPosition();
-    } else {
-      return null;
-    }
-  }
-
   static getDerivedStateFromProps(nextProps, prevState) {
     if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
       return {
@@ -126,32 +114,6 @@ class Status extends ImmutablePureComponent {
     }
   }
 
-  // Compensate height changes
-  componentDidUpdate (prevProps, prevState, snapshot) {
-    const doShowCard  = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
-
-    if (doShowCard && !this.didShowCard) {
-      this.didShowCard = true;
-
-      if (snapshot !== null && this.props.updateScrollBottom) {
-        if (this.node && this.node.offsetTop < snapshot.top) {
-          this.props.updateScrollBottom(snapshot.height - snapshot.top);
-        }
-      }
-    }
-  }
-
-  componentWillUnmount() {
-    if (this.node && this.props.getScrollPosition) {
-      const position = this.props.getScrollPosition();
-      if (position !== null && this.node.offsetTop < position.top) {
-        requestAnimationFrame(() => {
-          this.props.updateScrollBottom(position.height - position.top);
-        });
-      }
-    }
-  }
-
   handleToggleMediaVisibility = () => {
     this.setState({ showMedia: !this.state.showMedia });
   }
@@ -196,7 +158,11 @@ class Status extends ImmutablePureComponent {
 
   handleExpandedToggle = () => {
     this.props.onToggleHidden(this._properStatus());
-  };
+  }
+
+  handleCollapsedToggle = isCollapsed => {
+    this.props.onToggleCollapsed(this._properStatus(), isCollapsed);
+  }
 
   renderLoadingMediaGallery () {
     return <div className='media-gallery' style={{ height: '110px' }} />;
@@ -466,7 +432,7 @@ class Status extends ImmutablePureComponent {
               </a>
             </div>
 
-            <StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable />
+            <StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} />
 
             {media}
 
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index d13091325..5d921fd41 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -23,11 +23,11 @@ export default class StatusContent extends React.PureComponent {
     onExpandedToggle: PropTypes.func,
     onClick: PropTypes.func,
     collapsable: PropTypes.bool,
+    onCollapsedToggle: PropTypes.func,
   };
 
   state = {
     hidden: true,
-    collapsed: null, //  `collapsed: null` indicates that an element doesn't need collapsing, while `true` or `false` indicates that it does (and is/isn't).
   };
 
   _updateStatusLinks () {
@@ -62,14 +62,16 @@ export default class StatusContent extends React.PureComponent {
       link.setAttribute('rel', 'noopener noreferrer');
     }
 
-    if (
-      this.props.collapsable
-      && this.props.onClick
-      && this.state.collapsed === null
-      && node.clientHeight > MAX_HEIGHT
-      && this.props.status.get('spoiler_text').length === 0
-    ) {
-      this.setState({ collapsed: true });
+    if (this.props.status.get('collapsed', null) === null) {
+      let collapsed =
+          this.props.collapsable
+          && this.props.onClick
+          && node.clientHeight > MAX_HEIGHT
+          && this.props.status.get('spoiler_text').length === 0;
+
+      if(this.props.onCollapsedToggle) this.props.onCollapsedToggle(collapsed);
+
+      this.props.status.set('collapsed', collapsed);
     }
   }
 
@@ -178,6 +180,7 @@ export default class StatusContent extends React.PureComponent {
     }
 
     const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
+    const renderReadMore = this.props.onClick && status.get('collapsed');
 
     const content = { __html: status.get('contentHtml') };
     const spoilerContent = { __html: status.get('spoilerHtml') };
@@ -185,7 +188,7 @@ export default class StatusContent extends React.PureComponent {
     const classNames = classnames('status__content', {
       'status__content--with-action': this.props.onClick && this.context.router,
       'status__content--with-spoiler': status.get('spoiler_text').length > 0,
-      'status__content--collapsed': this.state.collapsed === true,
+      'status__content--collapsed': renderReadMore,
     });
 
     if (isRtl(status.get('search_index'))) {
@@ -237,7 +240,7 @@ export default class StatusContent extends React.PureComponent {
         </div>,
       ];
 
-      if (this.state.collapsed) {
+      if (renderReadMore) {
         output.push(readMoreButton);
       }
 
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index 35c16a20c..2ba3a3123 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -23,6 +23,7 @@ import {
   deleteStatus,
   hideStatus,
   revealStatus,
+  toggleStatusCollapse,
 } from '../actions/statuses';
 import {
   unmuteAccount,
@@ -190,6 +191,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }
   },
 
+  onToggleCollapsed (status, isCollapsed) {
+    dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
+  },
+
   onBlockDomain (domain) {
     dispatch(openModal('CONFIRM', {
       message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index 772f98bcb..398a48cff 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -12,6 +12,7 @@ import {
   STATUS_UNMUTE_SUCCESS,
   STATUS_REVEAL,
   STATUS_HIDE,
+  STATUS_COLLAPSE,
 } from '../actions/statuses';
 import { TIMELINE_DELETE } from '../actions/timelines';
 import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
@@ -73,6 +74,8 @@ export default function statuses(state = initialState, action) {
         }
       });
     });
+  case STATUS_COLLAPSE:
+    return state.setIn([action.id, 'collapsed'], action.isCollapsed);
   case TIMELINE_DELETE:
     return deleteStatus(state, action.id, action.references);
   default:
diff --git a/app/javascript/mastodon/utils/numbers.js b/app/javascript/mastodon/utils/numbers.js
index f7e4ceb93..af18dcfdd 100644
--- a/app/javascript/mastodon/utils/numbers.js
+++ b/app/javascript/mastodon/utils/numbers.js
@@ -4,9 +4,13 @@ import { FormattedNumber } from 'react-intl';
 export const shortNumberFormat = number => {
   if (number < 1000) {
     return <FormattedNumber value={number} />;
-  } else if (number < 1000000) {
+  } else if (number < 10000) {
     return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>;
-  } else {
+  } else if (number < 1000000) {
+    return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={0} />K</Fragment>;
+  } else if (number < 10000000) {
     return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={1} />M</Fragment>;
+  } else {
+    return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={0} />M</Fragment>;
   }
 };
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 01a633c5f..1a5d9e3e3 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -6399,13 +6399,13 @@ noscript {
     &__links {
       font-size: 14px;
       color: $darker-text-color;
+      padding: 10px 0;
 
       a {
         display: inline-block;
         color: $darker-text-color;
         text-decoration: none;
-        padding: 10px;
-        padding-top: 20px;
+        padding: 5px 10px;
         font-weight: 500;
 
         strong {
diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index 6a299f59d..e07ebfffe 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -78,7 +78,7 @@ class SearchQueryTransformer < Parslet::Transform
     elsif clause[:shortcode]
       TermClause.new(prefix, operator, ":#{clause[:term]}:")
     elsif clause[:phrase]
-      PhraseClause.new(prefix, operator, clause[:phrase].map { |p| p[:term].to_s }.join(' '))
+      PhraseClause.new(prefix, operator, clause[:phrase].is_a?(Array) ? clause[:phrase].map { |p| p[:term].to_s }.join(' ') : clause[:phrase].to_s)
     else
       raise "Unexpected clause type: #{clause}"
     end
diff --git a/app/middleware/handle_bad_encoding_middleware.rb b/app/middleware/handle_bad_encoding_middleware.rb
new file mode 100644
index 000000000..6fce84b15
--- /dev/null
+++ b/app/middleware/handle_bad_encoding_middleware.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+# See: https://jamescrisp.org/2018/05/28/fixing-invalid-query-parameters-invalid-encoding-in-a-rails-app/
+
+class HandleBadEncodingMiddleware
+  def initialize(app)
+    @app = app
+  end
+
+  def call(env)
+    begin
+      Rack::Utils.parse_nested_query(env['QUERY_STRING'].to_s)
+    rescue Rack::Utils::InvalidParameterError
+      env['QUERY_STRING'] = ''
+    end
+
+    @app.call(env)
+  end
+end
diff --git a/app/models/account.rb b/app/models/account.rb
index 25cde6d6c..86f7295bc 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -50,7 +50,7 @@
 
 class Account < ApplicationRecord
   USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
-  MENTION_RE  = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
+  MENTION_RE  = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[a-z0-9]+)?)/i
 
   include AccountAssociations
   include AccountAvatar
@@ -168,6 +168,10 @@ class Account < ApplicationRecord
     local? ? username : "#{username}@#{domain}"
   end
 
+  def pretty_acct
+    local? ? username : "#{username}@#{Addressable::IDNA.to_unicode(domain)}"
+  end
+
   def local_username_and_domain
     "#{username}@#{Rails.configuration.x.local_domain}"
   end
diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb
index 3bbc6453c..1e8c4806f 100644
--- a/app/models/concerns/attachmentable.rb
+++ b/app/models/concerns/attachmentable.rb
@@ -9,6 +9,7 @@ module Attachmentable
   GIF_MATRIX_LIMIT = 921_600    # 1280x720px
 
   included do
+    before_post_process :obfuscate_file_name
     before_post_process :set_file_extensions
     before_post_process :check_image_dimensions
     before_post_process :set_file_content_type
@@ -68,4 +69,14 @@ module Attachmentable
   rescue Terrapin::CommandLineError
     ''
   end
+
+  def obfuscate_file_name
+    self.class.attachment_definitions.each_key do |attachment_name|
+      attachment = send(attachment_name)
+
+      next if attachment.blank?
+
+      attachment.instance_write :file_name, SecureRandom.hex(8) + File.extname(attachment.instance_read(:file_name))
+    end
+  end
 end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 880599028..b87b1b9d3 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -202,9 +202,12 @@ class MediaAttachment < ApplicationRecord
   end
 
   after_commit :reset_parent_cache, on: :update
+
   before_create :prepare_description, unless: :local?
   before_create :set_shortcode
+
   before_post_process :set_type_and_extension
+
   before_save :set_meta
 
   class << self
diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb
index 7bdb5d7ff..657dd36f3 100644
--- a/app/serializers/rest/account_serializer.rb
+++ b/app/serializers/rest/account_serializer.rb
@@ -24,6 +24,10 @@ class REST::AccountSerializer < ActiveModel::Serializer
     object.id.to_s
   end
 
+  def acct
+    object.pretty_acct
+  end
+
   def note
     Formatter.instance.simplified_format(object)
   end
diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb
index 0b57b6d0c..ab6d090a0 100644
--- a/app/services/backup_service.rb
+++ b/app/services/backup_service.rb
@@ -166,7 +166,7 @@ class BackupService < BaseService
         io.write(buffer)
       end
     end
-  rescue Errno::ENOENT
+  rescue Errno::ENOENT, Seahorse::Client::NetworkingError
     Rails.logger.warn "Could not backup file #{filename}: file not found"
   end
 end
diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb
index 19de37717..3c257451c 100644
--- a/app/services/process_mentions_service.rb
+++ b/app/services/process_mentions_service.rb
@@ -14,7 +14,16 @@ class ProcessMentionsService < BaseService
     mentions = []
 
     status.text = status.text.gsub(Account::MENTION_RE) do |match|
-      username, domain  = Regexp.last_match(1).split('@')
+      username, domain = Regexp.last_match(1).split('@')
+
+      domain = begin
+        if TagManager.instance.local_domain?(domain)
+          nil
+        else
+          TagManager.instance.normalize_domain(domain)
+        end
+      end
+
       mentioned_account = Account.find_remote(username, domain)
 
       if mention_undeliverable?(mentioned_account)
diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb
index 79b1bad0c..1a2b0d60c 100644
--- a/app/services/resolve_url_service.rb
+++ b/app/services/resolve_url_service.rb
@@ -12,6 +12,8 @@ class ResolveURLService < BaseService
       process_local_url
     elsif !fetched_resource.nil?
       process_url
+    elsif @on_behalf_of.present?
+      process_url_from_db
     end
   end
 
@@ -24,15 +26,19 @@ class ResolveURLService < BaseService
       status = FetchRemoteStatusService.new.call(resource_url, body)
       authorize_with @on_behalf_of, status, :show? unless status.nil?
       status
-    elsif fetched_resource.nil? && @on_behalf_of.present?
-      # It may happen that the resource is a private toot, and thus not fetchable,
-      # but we can return the toot if we already know about it.
-      status = Status.find_by(uri: @url) || Status.find_by(url: @url)
-      authorize_with @on_behalf_of, status, :show? unless status.nil?
-      status
     end
   end
 
+  def process_url_from_db
+    # It may happen that the resource is a private toot, and thus not fetchable,
+    # but we can return the toot if we already know about it.
+    status = Status.find_by(uri: @url) || Status.find_by(url: @url)
+    authorize_with @on_behalf_of, status, :show? unless status.nil?
+    status
+  rescue Mastodon::NotPermittedError
+    nil
+  end
+
   def fetched_resource
     @fetched_resource ||= FetchResourceService.new.call(@url)
   end
diff --git a/app/views/admin/tags/_tag.html.haml b/app/views/admin/tags/_tag.html.haml
index 670f3bc05..287d28e53 100644
--- a/app/views/admin/tags/_tag.html.haml
+++ b/app/views/admin/tags/_tag.html.haml
@@ -1,6 +1,7 @@
 .batch-table__row
-  %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
-    = f.check_box :tag_ids, { multiple: true, include_hidden: false }, tag.id
+  - if batch_available
+    %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
+      = f.check_box :tag_ids, { multiple: true, include_hidden: false }, tag.id
 
   .directory__tag
     = link_to admin_tag_path(tag.id) do
diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml
index 298ac59e9..aa4f4c297 100644
--- a/app/views/admin/tags/index.html.haml
+++ b/app/views/admin/tags/index.html.haml
@@ -47,25 +47,26 @@
 
   .batch-table.optional
     .batch-table__toolbar
-      %label.batch-table__toolbar__select.batch-checkbox-all
-        = check_box_tag :batch_checkbox_all, nil, false
-      .batch-table__toolbar__actions
-        - if params[:pending_review] == '1'
+      - if params[:pending_review] == '1' || params[:unreviewed] == '1'
+        %label.batch-table__toolbar__select.batch-checkbox-all
+          = check_box_tag :batch_checkbox_all, nil, false
+        .batch-table__toolbar__actions
           = f.button safe_join([fa_icon('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
 
           = f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
-        - else
+      - else
+        .batch-table__toolbar__actions
           %span.neutral-hint= t('generic.no_batch_actions_available')
 
     .batch-table__body
       - if @tags.empty?
         = nothing_here 'nothing-here--under-tabs'
       - else
-        = render partial: 'tag', collection: @tags, locals: { f: f }
+        = render partial: 'tag', collection: @tags, locals: { f: f, batch_available: params[:pending_review] == '1' || params[:unreviewed] == '1' }
 
 = paginate @tags
 
-- if params[:pending_review] == '1'
+- if params[:pending_review] == '1' || params[:unreviewed] == '1'
   %hr.spacer/
 
   %div{ style: 'overflow: hidden' }
diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml
index f1e3d2e97..f460cebba 100644
--- a/app/views/settings/preferences/appearance/show.html.haml
+++ b/app/views/settings/preferences/appearance/show.html.haml
@@ -5,6 +5,10 @@
   .fields-group
     = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, selected: I18n.locale, hint: false
 
+  - unless I18n.locale == :en
+    .flash-message{ style: "text-align: unset; color: unset" }
+      #{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: "_blank", rel: "noopener", style: "text-decoration: underline")}
+
   %h4= t 'appearance.advanced_web_interface'
 
   %p.hint= t 'appearance.advanced_web_interface_hint'
diff --git a/app/views/statuses/embed.html.haml b/app/views/statuses/embed.html.haml
index 6f2ec646f..2f111f53f 100644
--- a/app/views/statuses/embed.html.haml
+++ b/app/views/statuses/embed.html.haml
@@ -1,3 +1,2 @@
-- cache @status do
-  .activity-stream.activity-stream--headless
-    = render 'status', status: @status, centered: true, autoplay: @autoplay
+.activity-stream.activity-stream--headless
+  = render 'status', status: @status, centered: true, autoplay: @autoplay
diff --git a/app/workers/refollow_worker.rb b/app/workers/refollow_worker.rb
index 12f2bf671..9b07ce1b5 100644
--- a/app/workers/refollow_worker.rb
+++ b/app/workers/refollow_worker.rb
@@ -7,15 +7,18 @@ class RefollowWorker
 
   def perform(target_account_id)
     target_account = Account.find(target_account_id)
-    return unless target_account.protocol == :activitypub
+    return unless target_account.activitypub?
+
+    target_account.passive_relationships.where(account: Account.where(domain: nil)).includes(:account).reorder(nil).find_each do |follow|
+      reblogs = follow.show_reblogs?
 
-    target_account.followers.where(domain: nil).reorder(nil).find_each do |follower|
       # Locally unfollow remote account
+      follower = follow.account
       follower.unfollow!(target_account)
 
       # Schedule re-follow
       begin
-        FollowService.new.call(follower, target_account)
+        FollowService.new.call(follower, target_account, reblogs: reblogs)
       rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError
         next
       end
diff --git a/config/application.rb b/config/application.rb
index e1f7ae707..58e59fd51 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -7,6 +7,7 @@ require 'rails/all'
 Bundler.require(*Rails.groups)
 
 require_relative '../app/lib/exceptions'
+require_relative '../app/middleware/handle_bad_encoding_middleware'
 require_relative '../lib/paperclip/lazy_thumbnail'
 require_relative '../lib/paperclip/gif_transcoder'
 require_relative '../lib/paperclip/video_transcoder'
@@ -118,6 +119,7 @@ module Mastodon
 
     config.active_job.queue_adapter = :sidekiq
 
+    config.middleware.insert_before Rack::Runtime, HandleBadEncodingMiddleware
     config.middleware.use Rack::Attack
     config.middleware.use Rack::Deflater
 
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 7784bec62..e03380cec 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -8,20 +8,15 @@ Doorkeeper.configure do
   end
 
   resource_owner_from_credentials do |_routes|
-    if Devise.ldap_authentication
-      user = User.authenticate_with_ldap({ :email => request.params[:username], :password => request.params[:password] })
-    end
-
-    if Devise.pam_authentication
-      user ||= User.authenticate_with_ldap({ :email => request.params[:username], :password => request.params[:password] })
-    end
+    user   = User.authenticate_with_ldap(email: request.params[:username], password: request.params[:password]) if Devise.ldap_authentication
+    user ||= User.authenticate_with_pam(email: request.params[:username], password: request.params[:password]) if Devise.pam_authentication
 
     if user.nil?
       user = User.find_by(email: request.params[:username])
-      user = nil unless user.valid_password?(request.params[:password])
+      user = nil unless user&.valid_password?(request.params[:password])
     end
 
-    user if !user&.otp_required_for_login?
+    user unless user&.otp_required_for_login?
   end
 
   # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index 5109baff7..8909678d6 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+Paperclip::DataUriAdapter.register
+
 Paperclip.interpolates :filename do |attachment, style|
   if style == :original
     attachment.original_filename
diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb
index 273cac9ca..3cd7ea3a6 100644
--- a/config/initializers/rack_attack.rb
+++ b/config/initializers/rack_attack.rb
@@ -46,10 +46,7 @@ class Rack::Attack
 
   PROTECTED_PATHS_REGEX = Regexp.union(PROTECTED_PATHS.map { |path| /\A#{Regexp.escape(path)}/ })
 
-  # Always allow requests from localhost
-  # (blocklist & throttles are skipped)
   Rack::Attack.safelist('allow from localhost') do |req|
-    # Requests are allowed if the return value is truthy
     req.remote_ip == '127.0.0.1' || req.remote_ip == '::1'
   end
 
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 43c24fc4e..74f397a3f 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -594,6 +594,10 @@ en:
     animations_and_accessibility: Animations and accessibility
     confirmation_dialogs: Confirmation dialogs
     discovery: Discovery
+    localization:
+      body: Mastodon is translated by volunteers.
+      guide_link: https://crowdin.com/project/mastodon
+      guide_link_text: Everyone can contribute.
     sensitive_content: Sensitive content
     toot_layout: Toot layout
   application_mailer:
diff --git a/db/migrate/20170711225116_fix_null_booleans.rb b/db/migrate/20170711225116_fix_null_booleans.rb
index 5b319471d..aabb81f21 100644
--- a/db/migrate/20170711225116_fix_null_booleans.rb
+++ b/db/migrate/20170711225116_fix_null_booleans.rb
@@ -1,17 +1,19 @@
 class FixNullBooleans < ActiveRecord::Migration[5.1]
   def change
-    change_column_default :domain_blocks, :reject_media, false
-    change_column_null :domain_blocks, :reject_media, false, false
+    safety_assured do
+      change_column_default :domain_blocks, :reject_media, false
+      change_column_null :domain_blocks, :reject_media, false, false
 
-    change_column_default :imports, :approved, false
-    change_column_null :imports, :approved, false, false
+      change_column_default :imports, :approved, false
+      change_column_null :imports, :approved, false, false
 
-    change_column_null :statuses, :sensitive, false, false
-    change_column_null :statuses, :reply, false, false
+      change_column_null :statuses, :sensitive, false, false
+      change_column_null :statuses, :reply, false, false
 
-    change_column_null :users, :admin, false, false
+      change_column_null :users, :admin, false, false
 
-    change_column_default :users, :otp_required_for_login, false
-    change_column_null :users, :otp_required_for_login, false, false
+      change_column_default :users, :otp_required_for_login, false
+      change_column_null :users, :otp_required_for_login, false, false
+    end
   end
 end
diff --git a/db/migrate/20171010025614_change_accounts_nonnullable_in_account_moderation_notes.rb b/db/migrate/20171010025614_change_accounts_nonnullable_in_account_moderation_notes.rb
index 747e5a826..1d7a0086c 100644
--- a/db/migrate/20171010025614_change_accounts_nonnullable_in_account_moderation_notes.rb
+++ b/db/migrate/20171010025614_change_accounts_nonnullable_in_account_moderation_notes.rb
@@ -1,6 +1,8 @@
 class ChangeAccountsNonnullableInAccountModerationNotes < ActiveRecord::Migration[5.1]
   def change
-    change_column_null :account_moderation_notes, :account_id, false
-    change_column_null :account_moderation_notes, :target_account_id, false
+    safety_assured do
+      change_column_null :account_moderation_notes, :account_id, false
+      change_column_null :account_moderation_notes, :target_account_id, false
+    end
   end
 end
diff --git a/db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb b/db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb
index 3369e3b9e..ac86c9e77 100644
--- a/db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb
+++ b/db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb
@@ -1,5 +1,7 @@
 class ChangeAccountIdNonnullableInLists < ActiveRecord::Migration[5.1]
   def change
-    change_column_null :lists, :account_id, false
+    safety_assured do
+      change_column_null :lists, :account_id, false
+    end
   end
 end
diff --git a/db/migrate/20180310000000_change_columns_in_notifications_nonnullable.rb b/db/migrate/20180310000000_change_columns_in_notifications_nonnullable.rb
index 05ffd0501..dba789207 100644
--- a/db/migrate/20180310000000_change_columns_in_notifications_nonnullable.rb
+++ b/db/migrate/20180310000000_change_columns_in_notifications_nonnullable.rb
@@ -1,8 +1,10 @@
 class ChangeColumnsInNotificationsNonnullable < ActiveRecord::Migration[5.1]
   def change
-    change_column_null :notifications, :activity_id, false
-    change_column_null :notifications, :activity_type, false
-    change_column_null :notifications, :account_id, false
-    change_column_null :notifications, :from_account_id, false
+    safety_assured do
+      change_column_null :notifications, :activity_id, false
+      change_column_null :notifications, :activity_type, false
+      change_column_null :notifications, :account_id, false
+      change_column_null :notifications, :from_account_id, false
+    end
   end
 end
diff --git a/package.json b/package.json
index 9e0c44550..71cab74ab 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
   },
   "private": true,
   "dependencies": {
-    "@babel/core": "^7.7.5",
+    "@babel/core": "^7.7.7",
     "@babel/plugin-proposal-class-properties": "^7.7.4",
     "@babel/plugin-proposal-decorators": "^7.7.4",
     "@babel/plugin-transform-react-inline-elements": "^7.7.4",
@@ -69,7 +69,7 @@
     "@babel/preset-react": "^7.7.4",
     "@babel/runtime": "^7.7.7",
     "@gamestdio/websocket": "^0.3.2",
-    "@clusterws/cws": "^0.16.0",
+    "@clusterws/cws": "^0.16.1",
     "array-includes": "^3.1.1",
     "atrament": "^0.2.3",
     "arrow-key-navigation": "^1.1.0",
@@ -107,7 +107,7 @@
     "intl": "^1.2.5",
     "intl-messageformat": "^2.2.0",
     "intl-relativeformat": "^6.4.3",
-    "is-nan": "^1.2.1",
+    "is-nan": "^1.3.0",
     "js-yaml": "^3.13.1",
     "lodash": "^4.17.14",
     "mark-loader": "^0.1.6",
@@ -146,7 +146,7 @@
     "react-textarea-autosize": "^7.1.2",
     "react-toggle": "^4.1.1",
     "redis": "^2.7.1",
-    "redux": "^4.0.4",
+    "redux": "^4.0.5",
     "redux-immutable": "^4.0.0",
     "redux-thunk": "^2.2.0",
     "rellax": "^1.10.0",
@@ -157,13 +157,13 @@
     "sass-loader": "^8.0.0",
     "stringz": "^2.0.0",
     "substring-trie": "^1.0.2",
-    "terser-webpack-plugin": "^2.2.2",
+    "terser-webpack-plugin": "^2.3.1",
     "tesseract.js": "^2.0.0-alpha.16",
     "throng": "^4.0.0",
     "tiny-queue": "^0.2.1",
     "uuid": "^3.3.3",
     "wavesurfer.js": "^3.2.0",
-    "webpack": "^4.41.2",
+    "webpack": "^4.41.5",
     "webpack-assets-manifest": "^3.1.1",
     "webpack-bundle-analyzer": "^3.6.0",
     "webpack-cli": "^3.3.10",
@@ -175,7 +175,7 @@
     "babel-jest": "^24.9.0",
     "enzyme": "^3.11.0",
     "enzyme-adapter-react-16": "^1.15.1",
-    "eslint": "^6.7.2",
+    "eslint": "^6.8.0",
     "eslint-plugin-import": "~2.19.1",
     "eslint-plugin-jsx-a11y": "~6.2.3",
     "eslint-plugin-promise": "~4.2.1",
@@ -183,7 +183,7 @@
     "jest": "^24.9.0",
     "raf": "^3.4.1",
     "react-intl-translations-manager": "^5.0.3",
-    "react-test-renderer": "^16.11.0",
+    "react-test-renderer": "^16.12.0",
     "sass-lint": "^1.13.1",
     "webpack-dev-server": "^3.9.0",
     "yargs": "^15.0.2"
diff --git a/spec/controllers/api/proofs_controller_spec.rb b/spec/controllers/api/proofs_controller_spec.rb
index dbde4927f..2fe615005 100644
--- a/spec/controllers/api/proofs_controller_spec.rb
+++ b/spec/controllers/api/proofs_controller_spec.rb
@@ -85,10 +85,7 @@ describe Api::ProofsController do
         end
 
         it 'has the correct avatar url' do
-          first_part = 'https://cb6e6126.ngrok.io/system/accounts/avatars/'
-          last_part  = 'original/avatar.gif'
-
-          expect(body_as_json[:avatar]).to match /#{Regexp.quote(first_part)}(?:\d{3,5}\/){3}#{Regexp.quote(last_part)}/
+          expect(body_as_json[:avatar]).to match "https://cb6e6126.ngrok.io#{alice.avatar.url}"
         end
       end
     end
diff --git a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
index 75e0570e9..54587187f 100644
--- a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
@@ -3,19 +3,38 @@ require 'rails_helper'
 describe Api::V1::Accounts::FollowerAccountsController do
   render_views
 
-  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:user)    { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:account) { Fabricate(:account) }
+  let(:alice)   { Fabricate(:account) }
+  let(:bob)     { Fabricate(:account) }
 
   before do
-    Fabricate(:follow, target_account: user.account)
+    alice.follow!(account)
+    bob.follow!(account)
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
   describe 'GET #index' do
     it 'returns http success' do
-      get :index, params: { account_id: user.account.id, limit: 1 }
+      get :index, params: { account_id: account.id, limit: 2 }
 
       expect(response).to have_http_status(200)
     end
+
+    it 'returns accounts following the given account' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(body_as_json.size).to eq 2
+      expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
+    end
+
+    it 'does not return blocked users' do
+      user.account.block!(bob)
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(body_as_json.size).to eq 1
+      expect(body_as_json[0][:id]).to eq alice.id.to_s
+    end
   end
 end
diff --git a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
index 7f7105ad3..a580a7368 100644
--- a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
@@ -3,19 +3,38 @@ require 'rails_helper'
 describe Api::V1::Accounts::FollowingAccountsController do
   render_views
 
-  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:user)    { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:account) { Fabricate(:account) }
+  let(:alice)   { Fabricate(:account) }
+  let(:bob)     { Fabricate(:account) }
 
   before do
-    Fabricate(:follow, account: user.account)
+    account.follow!(alice)
+    account.follow!(bob)
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
   describe 'GET #index' do
     it 'returns http success' do
-      get :index, params: { account_id: user.account.id, limit: 1 }
+      get :index, params: { account_id: account.id, limit: 2 }
 
       expect(response).to have_http_status(200)
     end
+
+    it 'returns accounts followed by the given account' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(body_as_json.size).to eq 2
+      expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
+    end
+
+    it 'does not return blocked users' do
+      user.account.block!(bob)
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(body_as_json.size).to eq 1
+      expect(body_as_json[0][:id]).to eq alice.id.to_s
+    end
   end
 end
diff --git a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
index 40f75c700..f053ae573 100644
--- a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
@@ -6,6 +6,8 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
   let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
   let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'read:accounts') }
+  let(:alice) { Fabricate(:account) }
+  let(:bob)   { Fabricate(:account) }
 
   context 'with an oauth token' do
     before do
@@ -16,14 +18,28 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
       let(:status) { Fabricate(:status, account: user.account) }
 
       before do
-        Fabricate(:favourite, status: status)
+        Favourite.create!(account: alice, status: status)
+        Favourite.create!(account: bob, status: status)
       end
 
       it 'returns http success' do
-        get :index, params: { status_id: status.id, limit: 1 }
+        get :index, params: { status_id: status.id, limit: 2 }
         expect(response).to have_http_status(200)
         expect(response.headers['Link'].links.size).to eq(2)
       end
+
+      it 'returns accounts who favorited the status' do
+        get :index, params: { status_id: status.id, limit: 2 }
+        expect(body_as_json.size).to eq 2
+      expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
+      end
+
+      it 'does not return blocked users' do
+        user.account.block!(bob)
+        get :index, params: { status_id: status.id, limit: 2 }
+        expect(body_as_json.size).to eq 1
+        expect(body_as_json[0][:id]).to eq alice.id.to_s
+      end
     end
   end
 
diff --git a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
index d758786dc..60908b7b3 100644
--- a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
@@ -6,6 +6,8 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
   let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
   let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'read:accounts') }
+  let(:alice) { Fabricate(:account) }
+  let(:bob)   { Fabricate(:account) }
 
   context 'with an oauth token' do
     before do
@@ -16,14 +18,28 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
       let(:status) { Fabricate(:status, account: user.account) }
 
       before do
-        Fabricate(:status, reblog_of_id: status.id)
+        Fabricate(:status, account: alice, reblog_of_id: status.id)
+        Fabricate(:status, account: bob, reblog_of_id: status.id)
       end
 
       it 'returns http success' do
-        get :index, params: { status_id: status.id, limit: 1 }
+        get :index, params: { status_id: status.id, limit: 2 }
         expect(response).to have_http_status(200)
         expect(response.headers['Link'].links.size).to eq(2)
       end
+
+      it 'returns accounts who reblogged the status' do
+        get :index, params: { status_id: status.id, limit: 2 }
+        expect(body_as_json.size).to eq 2
+      expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
+      end
+
+      it 'does not return blocked users' do
+        user.account.block!(bob)
+        get :index, params: { status_id: status.id, limit: 2 }
+        expect(body_as_json.size).to eq 1
+        expect(body_as_json[0][:id]).to eq alice.id.to_s
+      end
     end
   end
 
diff --git a/spec/controllers/concerns/obfuscate_filename_spec.rb b/spec/controllers/concerns/obfuscate_filename_spec.rb
deleted file mode 100644
index e06d53c03..000000000
--- a/spec/controllers/concerns/obfuscate_filename_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe ApplicationController, type: :controller do
-  controller do
-    include ObfuscateFilename
-
-    obfuscate_filename :file
-
-    def file
-      render plain: params[:file]&.original_filename
-    end
-  end
-
-  before do
-    routes.draw { get 'file' => 'anonymous#file' }
-  end
-
-  it 'obfusticates filename if the given parameter is specified' do
-    file = fixture_file_upload('files/imports.txt', 'text/plain')
-    post 'file', params: { file: file }
-    expect(response.body).to end_with '.txt'
-    expect(response.body).not_to include 'imports'
-  end
-
-  it 'does nothing if the given parameter is not specified' do
-    post 'file'
-  end
-end
diff --git a/spec/controllers/follower_accounts_controller_spec.rb b/spec/controllers/follower_accounts_controller_spec.rb
index 83032ab64..34a0cf3f4 100644
--- a/spec/controllers/follower_accounts_controller_spec.rb
+++ b/spec/controllers/follower_accounts_controller_spec.rb
@@ -22,6 +22,18 @@ describe FollowerAccountsController do
         expect(assigned[0]).to eq follow1
         expect(assigned[1]).to eq follow0
       end
+
+      it 'does not assign blocked users' do
+        user = Fabricate(:user)
+        user.account.block!(follower0)
+        sign_in(user)
+
+        expect(response).to have_http_status(200)
+
+        assigned = assigns(:follows).to_a
+        expect(assigned.size).to eq 1
+        expect(assigned[0]).to eq follow1
+      end
     end
 
     context 'when format is json' do
diff --git a/spec/controllers/following_accounts_controller_spec.rb b/spec/controllers/following_accounts_controller_spec.rb
index d5e4ee587..e9a1f597d 100644
--- a/spec/controllers/following_accounts_controller_spec.rb
+++ b/spec/controllers/following_accounts_controller_spec.rb
@@ -22,6 +22,18 @@ describe FollowingAccountsController do
         expect(assigned[0]).to eq follow1
         expect(assigned[1]).to eq follow0
       end
+
+      it 'does not assign blocked users' do
+        user = Fabricate(:user)
+        user.account.block!(followee0)
+        sign_in(user)
+
+        expect(response).to have_http_status(200)
+
+        assigned = assigns(:follows).to_a
+        expect(assigned.size).to eq 1
+        expect(assigned[0]).to eq follow1
+      end
     end
 
     context 'when format is json' do
diff --git a/spec/features/log_in_spec.rb b/spec/features/log_in_spec.rb
index b874c255b..de1a6de03 100644
--- a/spec/features/log_in_spec.rb
+++ b/spec/features/log_in_spec.rb
@@ -1,47 +1,51 @@
-require "rails_helper"
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+feature 'Log in' do
+  include ProfileStories
 
-feature "Log in" do
   given(:email)        { "test@example.com" }
   given(:password)     { "password" }
   given(:confirmed_at) { Time.zone.now }
 
   background do
-    Fabricate(:user, email: email, password: password, confirmed_at: confirmed_at)
+    as_a_registered_user
     visit new_user_session_path
   end
 
   subject { page }
 
-  scenario "A valid email and password user is able to log in" do
-    fill_in "user_email", with: email
-    fill_in "user_password", with: password
+  scenario 'A valid email and password user is able to log in' do
+    fill_in 'user_email', with: email
+    fill_in 'user_password', with: password
     click_on I18n.t('auth.login')
 
-    is_expected.to have_css("div.app-holder")
+    is_expected.to have_css('div.app-holder')
   end
 
-  scenario "A invalid email and password user is not able to log in" do
-    fill_in "user_email", with: "invalid_email"
-    fill_in "user_password", with: "invalid_password"
+  scenario 'A invalid email and password user is not able to log in' do
+    fill_in 'user_email', with: 'invalid_email'
+    fill_in 'user_password', with: 'invalid_password'
     click_on I18n.t('auth.login')
 
-    is_expected.to have_css(".flash-message", text: failure_message("invalid"))
+    is_expected.to have_css('.flash-message', text: failure_message('invalid'))
   end
 
   context do
     given(:confirmed_at) { nil }
 
-    scenario "A unconfirmed user is able to log in" do
-      fill_in "user_email", with: email
-      fill_in "user_password", with: password
+    scenario 'A unconfirmed user is able to log in' do
+      fill_in 'user_email', with: email
+      fill_in 'user_password', with: password
       click_on I18n.t('auth.login')
 
-      is_expected.to have_css("div.admin-wrapper")
+      is_expected.to have_css('div.admin-wrapper')
     end
   end
 
   def failure_message(message)
     keys = User.authentication_keys.map { |key| User.human_attribute_name(key) }
-    I18n.t("devise.failure.#{message}", authentication_keys: keys.join("support.array.words_connector"))
+    I18n.t("devise.failure.#{message}", authentication_keys: keys.join('support.array.words_connector'))
   end
 end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
new file mode 100644
index 000000000..3202167ca
--- /dev/null
+++ b/spec/features/profile_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+feature 'Profile' do
+  include ProfileStories
+
+  given(:local_domain) { ENV['LOCAL_DOMAIN'] }
+
+  background do
+    as_a_logged_in_user
+    with_alice_as_local_user
+  end
+
+  subject { page }
+
+  scenario 'I can view Annes public account' do
+    visit account_path('alice')
+
+    is_expected.to have_title("alice (@alice@#{local_domain})")
+
+    within('.public-account-header h1') do
+      is_expected.to have_content("alice @alice@#{local_domain}")
+    end
+
+    bio_elem = first('.public-account-bio')
+    expect(bio_elem).to have_content(alice_bio)
+    # The bio has hashtags made clickable
+    expect(bio_elem).to have_link('cryptology')
+    expect(bio_elem).to have_link('science')
+    # Nicknames are make clickable
+    expect(bio_elem).to have_link('@alice')
+    expect(bio_elem).to have_link('@bob')
+    # Nicknames not on server are not clickable
+    expect(bio_elem).not_to have_link('@pepe')
+  end
+
+  scenario 'I can change my account' do
+    visit settings_profile_path
+    fill_in 'Display name', with: 'Bob'
+    fill_in 'Bio', with: 'Bob is silent'
+    click_on 'Save changes'
+    is_expected.to have_content 'Changes successfully saved!'
+
+    # View my own public profile and see the changes
+    click_link "Bob @bob@#{local_domain}"
+
+    within('.public-account-header h1') do
+      is_expected.to have_content("Bob @bob@#{local_domain}")
+    end
+    expect(first('.public-account-bio')).to have_content('Bob is silent')
+  end
+end
diff --git a/spec/middleware/handle_bad_encoding_middleware_spec.rb b/spec/middleware/handle_bad_encoding_middleware_spec.rb
new file mode 100644
index 000000000..8c0d24f18
--- /dev/null
+++ b/spec/middleware/handle_bad_encoding_middleware_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+RSpec.describe HandleBadEncodingMiddleware do
+  let(:app) { double() }
+  let(:middleware) { HandleBadEncodingMiddleware.new(app) }
+
+  it "request with query string is unchanged" do
+    expect(app).to receive(:call).with("PATH" => "/some/path", "QUERY_STRING" => "name=fred")
+    middleware.call("PATH" => "/some/path", "QUERY_STRING" => "name=fred")
+  end
+
+  it "request with no query string is unchanged" do
+    expect(app).to receive(:call).with("PATH" => "/some/path")
+    middleware.call("PATH" => "/some/path")
+  end
+
+  it "request with invalid encoding in query string drops query string" do
+    expect(app).to receive(:call).with("QUERY_STRING" => "", "PATH" => "/some/path")
+    middleware.call("QUERY_STRING" => "q=%2Fsearch%2Fall%Forder%3Ddescending%26page%3D5%26sort%3Dcreated_at", "PATH" => "/some/path")
+  end
+end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index b2f6234cb..3cca9b343 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -823,4 +823,5 @@ RSpec.describe Account, type: :model do
   end
 
   include_examples 'AccountAvatar', :account
+  include_examples 'AccountHeader', :account
 end
diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb
index 7ddfba7ed..a275621a1 100644
--- a/spec/models/media_attachment_spec.rb
+++ b/spec/models/media_attachment_spec.rb
@@ -133,6 +133,24 @@ RSpec.describe MediaAttachment, type: :model do
       expect(media.file.meta["small"]["height"]).to eq 327
       expect(media.file.meta["small"]["aspect"]).to eq 490.0 / 327
     end
+
+    it 'gives the file a random name' do
+      expect(media.file_file_name).to_not eq 'attachment.jpg'
+    end
+  end
+
+  describe 'base64-encoded jpeg' do
+    let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" }
+    let(:media) { MediaAttachment.create(account: Fabricate(:account), file: base64_attachment) }
+
+    it 'saves media attachment' do
+      expect(media.persisted?).to be true
+      expect(media.file).to_not be_nil
+    end
+
+    it 'gives the file a file name' do
+      expect(media.file_file_name).to_not be_blank
+    end
   end
 
   describe 'descriptions for remote attachments' do
diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb
index b1abd79b0..c30de8eeb 100644
--- a/spec/services/process_mentions_service_spec.rb
+++ b/spec/services/process_mentions_service_spec.rb
@@ -5,11 +5,11 @@ RSpec.describe ProcessMentionsService, type: :service do
   let(:visibility) { :public }
   let(:status)     { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}", visibility: visibility) }
 
+  subject { ProcessMentionsService.new }
+
   context 'OStatus with public toot' do
     let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }
 
-    subject { ProcessMentionsService.new }
-
     before do
       stub_request(:post, remote_user.salmon_url)
       subject.call(status)
@@ -24,8 +24,6 @@ RSpec.describe ProcessMentionsService, type: :service do
     let(:visibility)  { :private }
     let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }
 
-    subject { ProcessMentionsService.new }
-
     before do
       stub_request(:post, remote_user.salmon_url)
       subject.call(status)
@@ -41,29 +39,45 @@ RSpec.describe ProcessMentionsService, type: :service do
   end
 
   context 'ActivityPub' do
-    let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
+    context do
+      let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
 
-    subject { ProcessMentionsService.new }
+      before do
+        stub_request(:post, remote_user.inbox_url)
+        subject.call(status)
+      end
 
-    before do
-      stub_request(:post, remote_user.inbox_url)
-      subject.call(status)
-    end
+      it 'creates a mention' do
+        expect(remote_user.mentions.where(status: status).count).to eq 1
+      end
 
-    it 'creates a mention' do
-      expect(remote_user.mentions.where(status: status).count).to eq 1
+      it 'sends activity to the inbox' do
+        expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
+      end
     end
 
-    it 'sends activity to the inbox' do
-      expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
+    context 'with an IDN domain' do
+      let(:remote_user) { Fabricate(:account, username: 'sneak', protocol: :activitypub, domain: 'xn--hresiar-mxa.ch', inbox_url: 'http://example.com/inbox') }
+      let(:status) { Fabricate(:status, account: account, text: "Hello @sneak@hæresiar.ch") }
+
+      before do
+        stub_request(:post, remote_user.inbox_url)
+        subject.call(status)
+      end
+
+      it 'creates a mention' do
+        expect(remote_user.mentions.where(status: status).count).to eq 1
+      end
+
+      it 'sends activity to the inbox' do
+        expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
+      end
     end
   end
 
   context 'Temporarily-unreachable ActivityPub user' do
     let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox', last_webfingered_at: nil) }
 
-    subject { ProcessMentionsService.new }
-
     before do
       stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404)
       stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:remote_user@example.com").to_return(status: 500)
diff --git a/spec/support/examples/models/concerns/account_avatar.rb b/spec/support/examples/models/concerns/account_avatar.rb
index f2a8a2459..2180f5273 100644
--- a/spec/support/examples/models/concerns/account_avatar.rb
+++ b/spec/support/examples/models/concerns/account_avatar.rb
@@ -16,4 +16,24 @@ shared_examples 'AccountAvatar' do |fabricator|
       end
     end
   end
+
+  describe 'base64-encoded files' do
+    let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" }
+    let(:account) { Fabricate(fabricator, avatar: base64_attachment) }
+
+    it 'saves avatar' do
+      expect(account.persisted?).to be true
+      expect(account.avatar).to_not be_nil
+    end
+
+    it 'gives the avatar a file name' do
+      expect(account.avatar_file_name).to_not be_blank
+    end
+
+    it 'saves a new avatar under a different file name' do
+      previous_file_name = account.avatar_file_name
+      account.update(avatar: base64_attachment)
+      expect(account.avatar_file_name).to_not eq previous_file_name
+    end
+  end
 end
diff --git a/spec/support/examples/models/concerns/account_header.rb b/spec/support/examples/models/concerns/account_header.rb
new file mode 100644
index 000000000..77ee0e629
--- /dev/null
+++ b/spec/support/examples/models/concerns/account_header.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+shared_examples 'AccountHeader' do |fabricator|
+  describe 'base64-encoded files' do
+    let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" }
+    let(:account) { Fabricate(fabricator, header: base64_attachment) }
+
+    it 'saves header' do
+      expect(account.persisted?).to be true
+      expect(account.header).to_not be_nil
+    end
+
+    it 'gives the header a file name' do
+      expect(account.header_file_name).to_not be_blank
+    end
+
+    it 'saves a new header under a different file name' do
+      previous_file_name = account.header_file_name
+      account.update(header: base64_attachment)
+      expect(account.header_file_name).to_not eq previous_file_name
+    end
+  end
+end
diff --git a/spec/support/stories/profile_stories.rb b/spec/support/stories/profile_stories.rb
new file mode 100644
index 000000000..75b413330
--- /dev/null
+++ b/spec/support/stories/profile_stories.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ProfileStories
+  attr_reader :bob, :alice, :alice_bio
+
+  def as_a_registered_user
+    @bob = Fabricate(
+      :user,
+      email: email, password: password, confirmed_at: confirmed_at,
+      account: Fabricate(:account, username: 'bob')
+    )
+  end
+
+  def as_a_logged_in_user
+    as_a_registered_user
+    visit new_user_session_path
+    fill_in 'user_email', with: email
+    fill_in 'user_password', with: password
+    click_on I18n.t('auth.login')
+  end
+
+  def with_alice_as_local_user
+    @alice_bio = '@alice and @bob are fictional characters commonly used as'\
+                 'placeholder names in #cryptology, as well as #science and'\
+                 'engineering 📖 literature. Not affilated with @pepe.'
+
+    @alice = Fabricate(
+      :user,
+      email: 'alice@example.com', password: password, confirmed_at: confirmed_at,
+      account: Fabricate(:account, username: 'alice', note: @alice_bio)
+    )
+  end
+
+  def confirmed_at
+    @confirmed_at ||= Time.zone.now
+  end
+
+  def email
+    @email ||= 'test@example.com'
+  end
+
+  def password
+    @password ||= 'password'
+  end
+end
diff --git a/spec/workers/refollow_worker_spec.rb b/spec/workers/refollow_worker_spec.rb
new file mode 100644
index 000000000..29771aa59
--- /dev/null
+++ b/spec/workers/refollow_worker_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe RefollowWorker do
+  subject { described_class.new }
+  let(:account) { Fabricate(:account, domain: 'example.org', protocol: :activitypub) }
+  let(:alice)   { Fabricate(:account, domain: nil, username: 'alice') }
+  let(:bob)     { Fabricate(:account, domain: nil, username: 'bob') }
+
+  describe 'perform' do
+    let(:service) { double }
+
+    before do
+      allow(FollowService).to receive(:new).and_return(service)
+      allow(service).to receive(:call)
+
+      alice.follow!(account, reblogs: true)
+      bob.follow!(account, reblogs: false)
+    end
+
+    it 'calls FollowService for local followers' do
+      result = subject.perform(account.id)
+
+      expect(result).to be_nil
+      expect(service).to have_received(:call).with(alice, account, reblogs: true)
+      expect(service).to have_received(:call).with(bob, account, reblogs: false)
+    end
+  end
+end
diff --git a/yarn.lock b/yarn.lock
index f7834e469..258567ddb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9,15 +9,15 @@
   dependencies:
     "@babel/highlight" "^7.0.0"
 
-"@babel/core@^7.1.0", "@babel/core@^7.4.5", "@babel/core@^7.7.5":
-  version "7.7.5"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.5.tgz#ae1323cd035b5160293307f50647e83f8ba62f7e"
-  integrity sha512-M42+ScN4+1S9iB6f+TL7QBpoQETxbclx+KNoKJABghnKYE+fMzSGqst0BZJc8CpI625bwPwYgUyRvxZ+0mZzpw==
+"@babel/core@^7.1.0", "@babel/core@^7.4.5", "@babel/core@^7.7.7":
+  version "7.7.7"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.7.tgz#ee155d2e12300bcc0cff6a8ad46f2af5063803e9"
+  integrity sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==
   dependencies:
     "@babel/code-frame" "^7.5.5"
-    "@babel/generator" "^7.7.4"
+    "@babel/generator" "^7.7.7"
     "@babel/helpers" "^7.7.4"
-    "@babel/parser" "^7.7.5"
+    "@babel/parser" "^7.7.7"
     "@babel/template" "^7.7.4"
     "@babel/traverse" "^7.7.4"
     "@babel/types" "^7.7.4"
@@ -29,10 +29,10 @@
     semver "^5.4.1"
     source-map "^0.5.0"
 
-"@babel/generator@^7.0.0", "@babel/generator@^7.7.4":
-  version "7.7.4"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.4.tgz#db651e2840ca9aa66f327dcec1dc5f5fa9611369"
-  integrity sha512-m5qo2WgdOJeyYngKImbkyQrnUN1mPceaG5BV+G0E3gWsa4l/jCSryWJdM2x8OuGAOyh+3d5pVYfZWCiNFtynxg==
+"@babel/generator@^7.0.0", "@babel/generator@^7.7.4", "@babel/generator@^7.7.7":
+  version "7.7.7"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.7.tgz#859ac733c44c74148e1a72980a64ec84b85f4f45"
+  integrity sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==
   dependencies:
     "@babel/types" "^7.7.4"
     jsesc "^2.5.1"
@@ -252,10 +252,10 @@
     esutils "^2.0.2"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.7.4", "@babel/parser@^7.7.5":
-  version "7.7.5"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.5.tgz#cbf45321619ac12d83363fcf9c94bb67fa646d71"
-  integrity sha512-KNlOe9+/nk4i29g0VXgl8PEXIRms5xKLJeuZ6UptN0fHv+jDiriG+y94X6qAgWTR0h3KaoM1wK5G5h7MHFRSig==
+"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.7.4", "@babel/parser@^7.7.7":
+  version "7.7.7"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.7.tgz#1b886595419cf92d811316d5b715a53ff38b4937"
+  integrity sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==
 
 "@babel/plugin-proposal-async-generator-functions@^7.7.4":
   version "7.7.4"
@@ -795,10 +795,10 @@
     lodash "^4.17.13"
     to-fast-properties "^2.0.0"
 
-"@clusterws/cws@^0.16.0":
-  version "0.16.0"
-  resolved "https://registry.yarnpkg.com/@clusterws/cws/-/cws-0.16.0.tgz#f6116cbf3a8b7ad0657916616ce5f8248746b797"
-  integrity sha512-YeGpAPIdkBsOnAkmFKVMWEjCKDH900U2if0B+nc1imfv+64AIb2JX2xiTA6BLDLppEgWV5c6bpWESjbHCNblHw==
+"@clusterws/cws@^0.16.1":
+  version "0.16.1"
+  resolved "https://registry.yarnpkg.com/@clusterws/cws/-/cws-0.16.1.tgz#443c47909601e87b881ffb2c4c83cbf27a228487"
+  integrity sha512-OamMsXwVppfmwX14Ed1msJJN0KNi+VBQh5AkAqUvIGTcJyHcjpsKjU15wS8QumvEewxx9gvGp7aSDop0/R5Gqg==
 
 "@cnakazawa/watch@^1.0.3":
   version "1.0.3"
@@ -3247,7 +3247,7 @@ default-gateway@^4.2.0:
     execa "^1.0.0"
     ip-regex "^2.1.0"
 
-define-properties@^1.1.1, define-properties@^1.1.2, define-properties@^1.1.3:
+define-properties@^1.1.2, define-properties@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
   integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
@@ -3942,10 +3942,10 @@ eslint@^2.7.0:
     text-table "~0.2.0"
     user-home "^2.0.0"
 
-eslint@^6.7.2:
-  version "6.7.2"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.7.2.tgz#c17707ca4ad7b2d8af986a33feba71e18a9fecd1"
-  integrity sha512-qMlSWJaCSxDFr8fBPvJM9kJwbazrhNcBU3+DszDW1OlEwKBBRWsJc7NJFelvwQpanHCR14cOLD41x8Eqvo3Nng==
+eslint@^6.8.0:
+  version "6.8.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
+  integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
   dependencies:
     "@babel/code-frame" "^7.0.0"
     ajv "^6.10.0"
@@ -4031,12 +4031,7 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
   integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
 
-esutils@^2.0.0:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
-  integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=
-
-esutils@^2.0.2:
+esutils@^2.0.0, esutils@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
@@ -4385,10 +4380,10 @@ find-cache-dir@^2.0.0, find-cache-dir@^2.1.0:
     make-dir "^2.0.0"
     pkg-dir "^3.0.0"
 
-find-cache-dir@^3.0.0, find-cache-dir@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.1.0.tgz#9935894999debef4cf9f677fdf646d002c4cdecb"
-  integrity sha512-zw+EFiNBNPgI2NTrKkDd1xd7q0cs6wr/iWnr/oUkI0yF9K9GqQ+riIt4aiyFaaqpaWbxPrJXHI+QvmNUQbX+0Q==
+find-cache-dir@^3.0.0, find-cache-dir@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.2.0.tgz#e7fe44c1abc1299f516146e563108fd1006c1874"
+  integrity sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==
   dependencies:
     commondir "^1.0.1"
     make-dir "^3.0.0"
@@ -5620,12 +5615,12 @@ is-my-json-valid@^2.10.0:
     jsonpointer "^4.0.0"
     xtend "^4.0.0"
 
-is-nan@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.2.1.tgz#9faf65b6fb6db24b7f5c0628475ea71f988401e2"
-  integrity sha1-n69ltvttskt/XAYoR16nH5iEAeI=
+is-nan@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03"
+  integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ==
   dependencies:
-    define-properties "^1.1.1"
+    define-properties "^1.1.3"
 
 is-number-object@^1.0.4:
   version "1.0.4"
@@ -8785,15 +8780,15 @@ react-swipeable-views@^0.13.3:
     react-swipeable-views-utils "^0.13.3"
     warning "^4.0.1"
 
-react-test-renderer@^16.0.0-0, react-test-renderer@^16.11.0:
-  version "16.11.0"
-  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.11.0.tgz#72574566496462c808ac449b0287a4c0a1a7d8f8"
-  integrity sha512-nh9gDl8R4ut+ZNNb2EeKO5VMvTKxwzurbSMuGBoKtjpjbg8JK/u3eVPVNi1h1Ue+eYK9oSzJjb+K3lzLxyA4ag==
+react-test-renderer@^16.0.0-0, react-test-renderer@^16.12.0:
+  version "16.12.0"
+  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.12.0.tgz#11417ffda579306d4e841a794d32140f3da1b43f"
+  integrity sha512-Vj/teSqt2oayaWxkbhQ6gKis+t5JrknXfPVo+aIJ8QwYAqMPH77uptOdrlphyxl8eQI/rtkOYg86i/UWkpFu0w==
   dependencies:
     object-assign "^4.1.1"
     prop-types "^15.6.2"
     react-is "^16.8.6"
-    scheduler "^0.17.0"
+    scheduler "^0.18.0"
 
 react-textarea-autosize@^7.1.2:
   version "7.1.2"
@@ -8946,10 +8941,10 @@ redux-thunk@^2.2.0:
   resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
   integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
 
-redux@^4.0.4:
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796"
-  integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==
+redux@^4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
+  integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
   dependencies:
     loose-envify "^1.4.0"
     symbol-observable "^1.2.0"
@@ -9395,14 +9390,6 @@ sax@^1.2.4, sax@~1.2.4:
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
 
-scheduler@^0.17.0:
-  version "0.17.0"
-  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.17.0.tgz#7c9c673e4ec781fac853927916d1c426b6f3ddfe"
-  integrity sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA==
-  dependencies:
-    loose-envify "^1.1.0"
-    object-assign "^4.1.1"
-
 scheduler@^0.18.0:
   version "0.18.0"
   resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4"
@@ -9487,12 +9474,7 @@ send@0.17.1:
     range-parser "~1.2.1"
     statuses "~1.5.0"
 
-serialize-javascript@^1.7.0:
-  version "1.9.1"
-  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb"
-  integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==
-
-serialize-javascript@^2.1.1, serialize-javascript@^2.1.2:
+serialize-javascript@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
   integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
@@ -10164,39 +10146,39 @@ tcomb@^2.5.0:
   resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-2.7.0.tgz#10d62958041669a5d53567b9a4ee8cde22b1c2b0"
   integrity sha1-ENYpWAQWaaXVNWe5pO6M3iKxwrA=
 
-terser-webpack-plugin@^1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4"
-  integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==
+terser-webpack-plugin@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c"
+  integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==
   dependencies:
     cacache "^12.0.2"
     find-cache-dir "^2.1.0"
     is-wsl "^1.1.0"
     schema-utils "^1.0.0"
-    serialize-javascript "^1.7.0"
+    serialize-javascript "^2.1.2"
     source-map "^0.6.1"
     terser "^4.1.2"
     webpack-sources "^1.4.0"
     worker-farm "^1.7.0"
 
-terser-webpack-plugin@^2.2.2:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.2.2.tgz#2a6e00237125564a455ad69b22e08ee59420473a"
-  integrity sha512-/CHMNswPMAwuD2kd++qys8UmBRmsshPSzHw4BlDwurPtK9YjeK93OV89YWkJulHk972cs07K/7Z92V6PNjWF8A==
+terser-webpack-plugin@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.1.tgz#6a63c27debc15b25ffd2588562ee2eeabdcab923"
+  integrity sha512-dNxivOXmDgZqrGxOttBH6B4xaxT4zNC+Xd+2K8jwGDMK5q2CZI+KZMA1AAnSRT+BTRvuzKsDx+fpxzPAmAMVcA==
   dependencies:
     cacache "^13.0.1"
-    find-cache-dir "^3.1.0"
+    find-cache-dir "^3.2.0"
     jest-worker "^24.9.0"
     schema-utils "^2.6.1"
-    serialize-javascript "^2.1.1"
+    serialize-javascript "^2.1.2"
     source-map "^0.6.1"
-    terser "^4.4.2"
+    terser "^4.4.3"
     webpack-sources "^1.4.3"
 
-terser@^4.1.2, terser@^4.4.2:
-  version "4.4.2"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-4.4.2.tgz#448fffad0245f4c8a277ce89788b458bfd7706e8"
-  integrity sha512-Uufrsvhj9O1ikwgITGsZ5EZS6qPokUOkCegS7fYOdGTv+OA90vndUbU6PEjr5ePqHfNUbGyMO7xyIZv2MhsALQ==
+terser@^4.1.2, terser@^4.4.3:
+  version "4.4.3"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-4.4.3.tgz#401abc52b88869cf904412503b1eb7da093ae2f0"
+  integrity sha512-0ikKraVtRDKGzHrzkCv5rUNDzqlhmhowOBqC0XqUHFpW+vJ45+20/IFBcebwKfiS2Z9fJin6Eo+F1zLZsxi8RA==
   dependencies:
     commander "^2.20.0"
     source-map "~0.6.1"
@@ -10858,10 +10840,10 @@ webpack-sources@^1.0.0, webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack-
     source-list-map "^2.0.0"
     source-map "~0.6.1"
 
-webpack@^4.41.2:
-  version "4.41.2"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.2.tgz#c34ec76daa3a8468c9b61a50336d8e3303dce74e"
-  integrity sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A==
+webpack@^4.41.5:
+  version "4.41.5"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.5.tgz#3210f1886bce5310e62bb97204d18c263341b77c"
+  integrity sha512-wp0Co4vpyumnp3KlkmpM5LWuzvZYayDwM2n17EHFr4qxBBbRokC7DJawPJC7TfSFZ9HZ6GsdH40EBj4UV0nmpw==
   dependencies:
     "@webassemblyjs/ast" "1.8.5"
     "@webassemblyjs/helper-module-context" "1.8.5"
@@ -10883,7 +10865,7 @@ webpack@^4.41.2:
     node-libs-browser "^2.2.1"
     schema-utils "^1.0.0"
     tapable "^1.1.3"
-    terser-webpack-plugin "^1.4.1"
+    terser-webpack-plugin "^1.4.3"
     watchpack "^1.6.0"
     webpack-sources "^1.4.1"