about summary refs log tree commit diff
diff options
context:
space:
mode:
authorDavid Yip <yipdw@member.fsf.org>2017-12-04 11:07:01 -0600
committerDavid Yip <yipdw@member.fsf.org>2017-12-04 11:07:01 -0600
commitd9800a5647cbc57db7679094b2271f8eb5ec328b (patch)
treef9210c465de5f9d80e294d9ffa8536f98f9c466e
parent1c74ede69e7a9916c19da6f05daa215231eba81c (diff)
parentf2f2f1032082d6212771bd0307136484f671d37e (diff)
Merge branch 'gs-master' into glitch-theme
-rw-r--r--.codeclimate.yml33
-rw-r--r--CODE_OF_CONDUCT.md2
-rw-r--r--Gemfile.lock87
-rw-r--r--app/controllers/admin/account_moderation_notes_controller.rb2
-rw-r--r--app/controllers/admin/accounts_controller.rb3
-rw-r--r--app/controllers/admin/action_logs_controller.rb9
-rw-r--r--app/controllers/admin/base_controller.rb1
-rw-r--r--app/controllers/admin/confirmations_controller.rb1
-rw-r--r--app/controllers/admin/custom_emojis_controller.rb8
-rw-r--r--app/controllers/admin/domain_blocks_controller.rb2
-rw-r--r--app/controllers/admin/email_domain_blocks_controller.rb4
-rw-r--r--app/controllers/admin/invites_controller.rb47
-rw-r--r--app/controllers/admin/reported_statuses_controller.rb6
-rw-r--r--app/controllers/admin/reports_controller.rb9
-rw-r--r--app/controllers/admin/resets_controller.rb1
-rw-r--r--app/controllers/admin/roles_controller.rb2
-rw-r--r--app/controllers/admin/settings_controller.rb3
-rw-r--r--app/controllers/admin/silences_controller.rb6
-rw-r--r--app/controllers/admin/statuses_controller.rb6
-rw-r--r--app/controllers/admin/suspensions_controller.rb2
-rw-r--r--app/controllers/admin/two_factor_authentications_controller.rb1
-rw-r--r--app/controllers/api/v1/accounts_controller.rb6
-rw-r--r--app/controllers/auth/registrations_controller.rb21
-rw-r--r--app/controllers/concerns/accountable_concern.rb9
-rw-r--r--app/controllers/invites_controller.rb43
-rw-r--r--app/controllers/settings/migrations_controller.rb33
-rw-r--r--app/helpers/admin/action_logs_helper.rb103
-rw-r--r--app/helpers/admin/filter_helper.rb3
-rw-r--r--app/helpers/jsonld_helper.rb18
-rw-r--r--app/javascript/flavours/glitch/components/status.js40
-rw-r--r--app/javascript/flavours/glitch/containers/status_container.js1
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/follow.js5
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/notification.js117
-rw-r--r--app/javascript/flavours/glitch/features/notifications/containers/notification_container.js1
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js4
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js17
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js1
-rw-r--r--app/javascript/flavours/glitch/styles/_mixins.scss1
-rw-r--r--app/javascript/flavours/glitch/styles/components.scss16
-rw-r--r--app/javascript/glitch/locales/pl.json44
-rw-r--r--app/javascript/mastodon/actions/accounts.js10
-rw-r--r--app/javascript/mastodon/actions/lists.js28
-rw-r--r--app/javascript/mastodon/actions/mutes.js4
-rw-r--r--app/javascript/mastodon/actions/streaming.js1
-rw-r--r--app/javascript/mastodon/actions/timelines.js2
-rw-r--r--app/javascript/mastodon/components/account.js2
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js8
-rw-r--r--app/javascript/mastodon/components/status_action_bar.js2
-rw-r--r--app/javascript/mastodon/features/account/components/action_bar.js12
-rw-r--r--app/javascript/mastodon/features/account/components/header.js7
-rw-r--r--app/javascript/mastodon/features/account_timeline/components/header.js9
-rw-r--r--app/javascript/mastodon/features/account_timeline/components/moved_note.js48
-rw-r--r--app/javascript/mastodon/features/account_timeline/containers/header_container.js8
-rw-r--r--app/javascript/mastodon/features/compose/components/navigation_bar.js2
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js2
-rw-r--r--app/javascript/mastodon/features/keyboard_shortcuts/index.js98
-rw-r--r--app/javascript/mastodon/features/list_timeline/index.js106
-rw-r--r--app/javascript/mastodon/features/status/components/action_bar.js2
-rw-r--r--app/javascript/mastodon/features/status/components/card.js36
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js3
-rw-r--r--app/javascript/mastodon/features/ui/components/image_loader.js4
-rw-r--r--app/javascript/mastodon/features/ui/components/media_modal.js2
-rw-r--r--app/javascript/mastodon/features/ui/index.js14
-rw-r--r--app/javascript/mastodon/features/ui/util/async-components.js8
-rw-r--r--app/javascript/mastodon/locales/ar.json26
-rw-r--r--app/javascript/mastodon/locales/bg.json26
-rw-r--r--app/javascript/mastodon/locales/ca.json44
-rw-r--r--app/javascript/mastodon/locales/de.json26
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json157
-rw-r--r--app/javascript/mastodon/locales/en.json26
-rw-r--r--app/javascript/mastodon/locales/eo.json26
-rw-r--r--app/javascript/mastodon/locales/es.json26
-rw-r--r--app/javascript/mastodon/locales/fa.json26
-rw-r--r--app/javascript/mastodon/locales/fi.json26
-rw-r--r--app/javascript/mastodon/locales/fr.json26
-rw-r--r--app/javascript/mastodon/locales/he.json26
-rw-r--r--app/javascript/mastodon/locales/hr.json26
-rw-r--r--app/javascript/mastodon/locales/hu.json26
-rw-r--r--app/javascript/mastodon/locales/id.json26
-rw-r--r--app/javascript/mastodon/locales/io.json26
-rw-r--r--app/javascript/mastodon/locales/it.json26
-rw-r--r--app/javascript/mastodon/locales/ja.json26
-rw-r--r--app/javascript/mastodon/locales/ko.json30
-rw-r--r--app/javascript/mastodon/locales/nl.json26
-rw-r--r--app/javascript/mastodon/locales/no.json26
-rw-r--r--app/javascript/mastodon/locales/oc.json26
-rw-r--r--app/javascript/mastodon/locales/pl.json26
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json26
-rw-r--r--app/javascript/mastodon/locales/pt.json26
-rw-r--r--app/javascript/mastodon/locales/ru.json26
-rw-r--r--app/javascript/mastodon/locales/sv.json34
-rw-r--r--app/javascript/mastodon/locales/th.json26
-rw-r--r--app/javascript/mastodon/locales/tr.json26
-rw-r--r--app/javascript/mastodon/locales/uk.json26
-rw-r--r--app/javascript/mastodon/locales/whitelist_sv.json2
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json49
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json26
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json26
-rw-r--r--app/javascript/mastodon/reducers/accounts.js5
-rw-r--r--app/javascript/mastodon/reducers/accounts_counters.js3
-rw-r--r--app/javascript/mastodon/reducers/contexts.js13
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/mastodon/reducers/lists.js15
-rw-r--r--app/javascript/mastodon/reducers/statuses.js19
-rw-r--r--app/javascript/mastodon/reducers/timelines.js5
-rw-r--r--app/javascript/mastodon/selectors/index.js8
-rw-r--r--app/javascript/styles/mastodon/admin.scss117
-rw-r--r--app/javascript/styles/mastodon/components.scss83
-rw-r--r--app/javascript/styles/mastodon/landing_strip.scss63
-rw-r--r--app/lib/activitypub/activity/create.rb53
-rw-r--r--app/lib/activitypub/activity/delete.rb7
-rw-r--r--app/lib/activitypub/adapter.rb1
-rw-r--r--app/lib/feed_manager.rb40
-rw-r--r--app/lib/formatter.rb15
-rw-r--r--app/models/account.rb12
-rw-r--r--app/models/admin.rb7
-rw-r--r--app/models/admin/action_log.rb40
-rw-r--r--app/models/concerns/account_interactions.rb4
-rw-r--r--app/models/form/admin_settings.rb4
-rw-r--r--app/models/form/migration.rb25
-rw-r--r--app/models/form/status_batch.rb8
-rw-r--r--app/models/glitch/keyword_mute.rb72
-rw-r--r--app/models/invite.rb52
-rw-r--r--app/models/invite_filter.rb32
-rw-r--r--app/models/notification.rb10
-rw-r--r--app/models/status.rb3
-rw-r--r--app/models/user.rb22
-rw-r--r--app/policies/invite_policy.rb25
-rw-r--r--app/serializers/activitypub/actor_serializer.rb8
-rw-r--r--app/serializers/rest/account_serializer.rb4
-rw-r--r--app/serializers/rest/list_serializer.rb4
-rw-r--r--app/services/activitypub/fetch_remote_status_service.rb4
-rw-r--r--app/services/activitypub/process_account_service.rb14
-rw-r--r--app/services/fetch_link_card_service.rb3
-rw-r--r--app/services/process_mentions_service.rb2
-rw-r--r--app/services/remove_status_service.rb12
-rw-r--r--app/views/accounts/_header.html.haml20
-rw-r--r--app/views/accounts/_moved_strip.html.haml17
-rw-r--r--app/views/accounts/show.html.haml2
-rw-r--r--app/views/admin/action_logs/_action_log.html.haml15
-rw-r--r--app/views/admin/action_logs/index.html.haml7
-rw-r--r--app/views/admin/invites/_invite.html.haml21
-rw-r--r--app/views/admin/invites/index.html.haml30
-rw-r--r--app/views/admin/settings/edit.html.haml8
-rw-r--r--app/views/auth/registrations/new.html.haml1
-rw-r--r--app/views/invites/_form.html.haml9
-rw-r--r--app/views/invites/_invite.html.haml17
-rw-r--r--app/views/invites/index.html.haml19
-rw-r--r--app/views/settings/migrations/show.html.haml17
-rw-r--r--app/views/settings/profiles/show.html.haml5
-rw-r--r--app/views/stream_entries/show.html.haml2
-rw-r--r--app/workers/activitypub/raw_distribution_worker.rb4
-rw-r--r--config/brakeman.ignore55
-rw-r--r--config/i18n-tasks.yml1
-rw-r--r--config/locales/activerecord.ca.yml2
-rw-r--r--config/locales/ca.yml426
-rw-r--r--config/locales/de.yml2
-rw-r--r--config/locales/devise.ca.yml76
-rw-r--r--config/locales/devise.zh-CN.yml12
-rw-r--r--config/locales/doorkeeper.ca.yml86
-rw-r--r--config/locales/doorkeeper.zh-CN.yml4
-rw-r--r--config/locales/en.yml86
-rw-r--r--config/locales/eo.yml2
-rw-r--r--config/locales/fa.yml2
-rw-r--r--config/locales/fr.yml2
-rw-r--r--config/locales/ja.yml83
-rw-r--r--config/locales/ko.yml2
-rw-r--r--config/locales/nl.yml2
-rw-r--r--config/locales/oc.yml2
-rw-r--r--config/locales/pl.yml80
-rw-r--r--config/locales/pt-BR.yml2
-rw-r--r--config/locales/ru.yml2
-rw-r--r--config/locales/simple_form.ca.yml19
-rw-r--r--config/locales/simple_form.en.yml2
-rw-r--r--config/locales/simple_form.ja.yml2
-rw-r--r--config/locales/simple_form.pl.yml2
-rw-r--r--config/locales/simple_form.zh-CN.yml1
-rw-r--r--config/locales/sv.yml2
-rw-r--r--config/locales/zh-CN.yml37
-rw-r--r--config/navigation.rb17
-rw-r--r--config/routes.rb8
-rw-r--r--config/settings.yml2
-rw-r--r--db/migrate/20171028221157_add_reblogs_to_follows.rb4
-rw-r--r--db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb6
-rw-r--r--db/migrate/20171119172437_create_admin_action_logs.rb12
-rw-r--r--db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb12
-rw-r--r--db/migrate/20171125024930_create_invites.rb15
-rw-r--r--db/migrate/20171125031751_add_invite_id_to_users.rb5
-rw-r--r--db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb7
-rw-r--r--db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb14
-rw-r--r--db/migrate/20171129172043_add_index_on_stream_entries.rb7
-rw-r--r--db/schema.rb36
-rw-r--r--package.json32
-rw-r--r--spec/fabricators/admin_action_log_fabricator.rb5
-rw-r--r--spec/fabricators/invite_fabricator.rb6
-rw-r--r--spec/lib/activitypub/activity/delete_spec.rb13
-rw-r--r--spec/lib/feed_manager_spec.rb16
-rw-r--r--spec/lib/settings/extend_spec.rb16
-rw-r--r--spec/models/admin/action_log_spec.rb5
-rw-r--r--spec/models/concerns/account_interactions_spec.rb592
-rw-r--r--spec/models/concerns/remotable_spec.rb205
-rw-r--r--spec/models/concerns/streamable_spec.rb63
-rw-r--r--spec/models/glitch/keyword_mute_spec.rb93
-rw-r--r--spec/models/invite_spec.rb30
-rw-r--r--spec/models/notification_spec.rb31
-rw-r--r--spec/models/status_spec.rb27
-rw-r--r--spec/models/user_spec.rb43
-rw-r--r--spec/presenters/account_relationships_presenter_spec.rb82
-rw-r--r--spec/services/activitypub/fetch_remote_status_service_spec.rb36
-rw-r--r--spec/services/notify_service_spec.rb20
-rw-r--r--spec/services/process_mentions_service_spec.rb21
-rw-r--r--streaming/index.js2
-rw-r--r--yarn.lock738
214 files changed, 5376 insertions, 1031 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
index 29701a777..47e3e6ab9 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -1,21 +1,36 @@
-engines:
+version: "2"
+checks:
+  argument-count:
+    enabled: false
+  complex-logic:
+    enabled: false
+  file-lines:
+    enabled: false
+  method-complexity:
+    enabled: false
+  method-count:
+    enabled: false
+  method-lines:
+    enabled: false
+  nested-control-flow:
+    enabled: false
+  return-statements:
+    enabled: false
+  similar-code:
+    enabled: false
+  identical-code:
+    enabled: false
+plugins:
   brakeman:
     enabled: true
   bundler-audit:
     enabled: true
-  duplication:
-    enabled: false
   eslint:
     enabled: true
   rubocop:
     enabled: true
   scss-lint:
     enabled: true
-ratings:
-  paths:
-  - "**.rb"
-  - "**.js"
-  - "**.scss"
-exclude_paths:
+exclude_patterns:
 - spec/
 - vendor/asset
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 7cec57180..4b20fe971 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
 
 ## Enforcement
 
-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eugen@zeonfederated.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beatrix.bitrot@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
 
 Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
 
diff --git a/Gemfile.lock b/Gemfile.lock
index f9c69d538..37fc77fdf 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -24,11 +24,11 @@ GEM
       erubi (~> 1.4)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.0.3)
-    active_model_serializers (0.10.6)
+    active_model_serializers (0.10.7)
       actionpack (>= 4.1, < 6)
       activemodel (>= 4.1, < 6)
       case_transform (>= 0.2)
-      jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
+      jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
     active_record_query_trace (1.5.4)
     activejob (5.1.4)
       activesupport (= 5.1.4)
@@ -83,15 +83,15 @@ GEM
     capistrano-bundler (1.3.0)
       capistrano (~> 3.1)
       sshkit (~> 1.2)
-    capistrano-rails (1.3.0)
+    capistrano-rails (1.3.1)
       capistrano (~> 3.1)
       capistrano-bundler (~> 1.1)
-    capistrano-rbenv (2.1.2)
+    capistrano-rbenv (2.1.3)
       capistrano (~> 3.1)
       sshkit (~> 1.3)
     capistrano-yarn (2.0.2)
       capistrano (~> 3.0)
-    capybara (2.15.4)
+    capybara (2.16.1)
       addressable
       mini_mime (>= 0.1.3)
       nokogiri (>= 1.3.3)
@@ -113,7 +113,7 @@ GEM
     connection_pool (2.2.1)
     crack (0.4.3)
       safe_yaml (~> 1.0.0)
-    crass (1.0.2)
+    crass (1.0.3)
     debug_inspector (0.0.3)
     devise (4.3.0)
       bcrypt (~> 3.0)
@@ -121,11 +121,11 @@ GEM
       railties (>= 4.1.0, < 5.2)
       responders
       warden (~> 1.2.3)
-    devise-two-factor (3.0.0)
-      activesupport
+    devise-two-factor (3.0.2)
+      activesupport (< 5.2)
       attr_encrypted (>= 1.3, < 4, != 2)
       devise (~> 4.0)
-      railties
+      railties (< 5.2)
       rotp (~> 2.0)
     diff-lcs (1.3)
     docile (1.1.5)
@@ -184,7 +184,7 @@ GEM
       http (~> 2.2)
       nokogiri (~> 1.8)
       oj (~> 3.0)
-    hamlit (2.8.4)
+    hamlit (2.8.5)
       temple (>= 0.8.0)
       thor
       tilt
@@ -196,7 +196,7 @@ GEM
     hamster (3.0.0)
       concurrent-ruby (~> 1.0)
     hashdiff (0.3.7)
-    highline (1.7.8)
+    highline (1.7.10)
     hiredis (0.6.1)
     hkdf (0.3.0)
     htmlentities (4.3.4)
@@ -213,9 +213,9 @@ GEM
     httplog (0.99.7)
       colorize
       rack
-    i18n (0.9.0)
+    i18n (0.9.1)
       concurrent-ruby (~> 1.0)
-    i18n-tasks (0.9.18)
+    i18n-tasks (0.9.19)
       activesupport (>= 4.0.2)
       ast (>= 2.1.0)
       easy_translate (>= 0.5.0)
@@ -236,8 +236,8 @@ GEM
       json-ld (~> 2.1, >= 2.1.5)
       multi_json (~> 1.11)
       rdf (~> 2.2)
-    jsonapi-renderer (0.1.3)
-    jwt (1.5.6)
+    jsonapi-renderer (0.2.0)
+    jwt (2.1.0)
     kaminari (1.1.1)
       activesupport (>= 4.1.0)
       kaminari-actionview (= 1.1.1)
@@ -267,8 +267,8 @@ GEM
     loofah (2.1.1)
       crass (~> 1.0.2)
       nokogiri (>= 1.5.9)
-    mail (2.6.6)
-      mime-types (>= 1.16, < 4)
+    mail (2.7.0)
+      mini_mime (>= 0.1.1)
     mario-redis-lock (1.2.0)
       redis (~> 3, >= 3.0.5)
     method_source (0.9.0)
@@ -279,7 +279,7 @@ GEM
       mime-types-data (~> 3.2015)
     mime-types-data (3.2016.0521)
     mimemagic (0.3.2)
-    mini_mime (0.1.4)
+    mini_mime (1.0.0)
     mini_portile2 (2.3.0)
     minitest (5.10.3)
     msgpack (1.1.0)
@@ -305,7 +305,7 @@ GEM
       http (~> 2.0)
       nokogiri (~> 1.6)
       openssl (~> 2.0)
-    ox (2.8.1)
+    ox (2.8.2)
     paperclip (5.1.0)
       activemodel (>= 4.2.0)
       activesupport (>= 4.2.0)
@@ -316,22 +316,22 @@ GEM
       av (~> 0.9.0)
       paperclip (>= 2.5.2)
     parallel (1.12.0)
-    parallel_tests (2.17.0)
+    parallel_tests (2.19.0)
       parallel
-    parser (2.4.0.0)
-      ast (~> 2.2)
+    parser (2.4.0.2)
+      ast (~> 2.3)
     pg (0.21.0)
     pghero (1.7.0)
       activerecord
     pkg-config (1.2.8)
     powerpack (0.1.1)
-    pry (0.11.2)
+    pry (0.11.3)
       coderay (~> 1.1.0)
       method_source (~> 0.9.0)
     pry-rails (0.3.6)
       pry (>= 0.10.4)
-    public_suffix (3.0.0)
-    puma (3.10.0)
+    public_suffix (3.0.1)
+    puma (3.11.0)
     pundit (1.1.0)
       activesupport (>= 3.0.0)
     rabl (0.13.1)
@@ -344,7 +344,7 @@ GEM
       rack
     rack-proxy (0.6.2)
       rack
-    rack-test (0.7.0)
+    rack-test (0.8.2)
       rack (>= 1.0, < 3)
     rack-timeout (0.4.2)
     rails (5.1.4)
@@ -381,8 +381,11 @@ GEM
       thor (>= 0.18.1, < 2.0)
     rainbow (2.2.2)
       rake
-    rake (12.2.1)
-    rdf (2.2.11)
+    rake (12.3.0)
+    rb-fsevent (0.10.2)
+    rb-inotify (0.9.10)
+      ffi (>= 0.5.0, < 2)
+    rdf (2.2.12)
       hamster (~> 3.0)
       link_header (~> 0.0, >= 0.0.8)
     rdf-normalize (0.3.2)
@@ -395,8 +398,8 @@ GEM
     redis-activesupport (5.0.4)
       activesupport (>= 3, < 6)
       redis-store (>= 1.3, < 2)
-    redis-namespace (1.5.3)
-      redis (~> 3.0, >= 3.0.4)
+    redis-namespace (1.6.0)
+      redis (>= 3.0.4)
     redis-rack (2.0.3)
       rack (>= 1.5, < 3)
       redis-store (>= 1.2, < 2)
@@ -421,7 +424,7 @@ GEM
     rspec-mocks (3.7.0)
       diff-lcs (>= 1.2.0, < 2.0)
       rspec-support (~> 3.7.0)
-    rspec-rails (3.7.1)
+    rspec-rails (3.7.2)
       actionpack (>= 3.0)
       activesupport (>= 3.0)
       railties (>= 3.0)
@@ -449,10 +452,14 @@ GEM
       crass (~> 1.0.2)
       nokogiri (>= 1.4.4)
       nokogumbo (~> 1.4.1)
-    sass (3.4.25)
-    scss_lint (0.55.0)
+    sass (3.5.3)
+      sass-listen (~> 4.0.0)
+    sass-listen (4.0.0)
+      rb-fsevent (~> 0.9, >= 0.9.4)
+      rb-inotify (~> 0.9, >= 0.9.7)
+    scss_lint (0.56.0)
       rake (>= 0.9, < 13)
-      sass (~> 3.4.20)
+      sass (~> 3.5.3)
     sidekiq (5.0.5)
       concurrent-ruby (~> 1.0)
       connection_pool (~> 2.2, >= 2.2.0)
@@ -486,7 +493,7 @@ GEM
       actionpack (>= 4.0)
       activesupport (>= 4.0)
       sprockets (>= 3.0.0)
-    sshkit (1.14.0)
+    sshkit (1.15.1)
       net-scp (>= 1.1.2)
       net-ssh (>= 2.8.0)
     statsd-ruby (1.2.1)
@@ -514,7 +521,7 @@ GEM
     uniform_notifier (1.10.0)
     warden (1.2.7)
       rack (>= 1.0)
-    webmock (3.1.0)
+    webmock (3.1.1)
       addressable (>= 2.3.6)
       crack (>= 0.3.2)
       hashdiff
@@ -522,12 +529,12 @@ GEM
       activesupport (>= 4.2)
       rack-proxy (>= 0.6.1)
       railties (>= 4.2)
-    webpush (0.3.2)
+    webpush (0.3.3)
       hkdf (~> 0.2)
-      jwt
+      jwt (~> 2.0)
     websocket-driver (0.6.5)
       websocket-extensions (>= 0.1.0)
-    websocket-extensions (0.1.2)
+    websocket-extensions (0.1.3)
     xpath (2.1.0)
       nokogiri (~> 1.3)
 
@@ -638,4 +645,4 @@ RUBY VERSION
    ruby 2.4.2p198
 
 BUNDLED WITH
-   1.15.4
+   1.16.0
diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb
index 7f69a3363..7d5b9bf52 100644
--- a/app/controllers/admin/account_moderation_notes_controller.rb
+++ b/app/controllers/admin/account_moderation_notes_controller.rb
@@ -21,7 +21,7 @@ module Admin
 
     def destroy
       authorize @account_moderation_note, :destroy?
-      @account_moderation_note.destroy
+      @account_moderation_note.destroy!
       redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
     end
 
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index 0829bc769..e9a512e70 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -32,18 +32,21 @@ module Admin
     def memorialize
       authorize @account, :memorialize?
       @account.memorialize!
+      log_action :memorialize, @account
       redirect_to admin_account_path(@account.id)
     end
 
     def enable
       authorize @account.user, :enable?
       @account.user.enable!
+      log_action :enable, @account.user
       redirect_to admin_account_path(@account.id)
     end
 
     def disable
       authorize @account.user, :disable?
       @account.user.disable!
+      log_action :disable, @account.user
       redirect_to admin_account_path(@account.id)
     end
 
diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb
new file mode 100644
index 000000000..e273dfeae
--- /dev/null
+++ b/app/controllers/admin/action_logs_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Admin
+  class ActionLogsController < BaseController
+    def index
+      @action_logs = Admin::ActionLog.page(params[:page])
+    end
+  end
+end
diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb
index 726134509..fc299f74c 100644
--- a/app/controllers/admin/base_controller.rb
+++ b/app/controllers/admin/base_controller.rb
@@ -3,6 +3,7 @@
 module Admin
   class BaseController < ApplicationController
     include Authorization
+    include AccountableConcern
 
     layout 'admin'
 
diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb
index c10b0ebee..34dfb458e 100644
--- a/app/controllers/admin/confirmations_controller.rb
+++ b/app/controllers/admin/confirmations_controller.rb
@@ -7,6 +7,7 @@ module Admin
     def create
       authorize @user, :confirm?
       @user.confirm!
+      log_action :confirm, @user
       redirect_to admin_accounts_path
     end
 
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb
index 509f7a48f..3fa2a0b72 100644
--- a/app/controllers/admin/custom_emojis_controller.rb
+++ b/app/controllers/admin/custom_emojis_controller.rb
@@ -20,6 +20,7 @@ module Admin
       @custom_emoji = CustomEmoji.new(resource_params)
 
       if @custom_emoji.save
+        log_action :create, @custom_emoji
         redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
       else
         render :new
@@ -30,6 +31,7 @@ module Admin
       authorize @custom_emoji, :update?
 
       if @custom_emoji.update(resource_params)
+        log_action :update, @custom_emoji
         redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
       else
         redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
@@ -38,7 +40,8 @@ module Admin
 
     def destroy
       authorize @custom_emoji, :destroy?
-      @custom_emoji.destroy
+      @custom_emoji.destroy!
+      log_action :destroy, @custom_emoji
       redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
     end
 
@@ -49,6 +52,7 @@ module Admin
       emoji.image = @custom_emoji.image
 
       if emoji.save
+        log_action :create, emoji
         flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
       else
         flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
@@ -60,12 +64,14 @@ module Admin
     def enable
       authorize @custom_emoji, :enable?
       @custom_emoji.update!(disabled: false)
+      log_action :enable, @custom_emoji
       redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
     end
 
     def disable
       authorize @custom_emoji, :disable?
       @custom_emoji.update!(disabled: true)
+      log_action :disable, @custom_emoji
       redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
     end
 
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index e383dc831..64de2cbf0 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -21,6 +21,7 @@ module Admin
 
       if @domain_block.save
         DomainBlockWorker.perform_async(@domain_block.id)
+        log_action :create, @domain_block
         redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
       else
         render :new
@@ -34,6 +35,7 @@ module Admin
     def destroy
       authorize @domain_block, :destroy?
       UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
+      log_action :destroy, @domain_block
       redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
     end
 
diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb
index 01058bf46..9fe85064e 100644
--- a/app/controllers/admin/email_domain_blocks_controller.rb
+++ b/app/controllers/admin/email_domain_blocks_controller.rb
@@ -20,6 +20,7 @@ module Admin
       @email_domain_block = EmailDomainBlock.new(resource_params)
 
       if @email_domain_block.save
+        log_action :create, @email_domain_block
         redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
       else
         render :new
@@ -28,7 +29,8 @@ module Admin
 
     def destroy
       authorize @email_domain_block, :destroy?
-      @email_domain_block.destroy
+      @email_domain_block.destroy!
+      log_action :destroy, @email_domain_block
       redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
     end
 
diff --git a/app/controllers/admin/invites_controller.rb b/app/controllers/admin/invites_controller.rb
new file mode 100644
index 000000000..faccaa7c8
--- /dev/null
+++ b/app/controllers/admin/invites_controller.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Admin
+  class InvitesController < BaseController
+    def index
+      authorize :invite, :index?
+
+      @invites = filtered_invites.includes(user: :account).page(params[:page])
+      @invite  = Invite.new
+    end
+
+    def create
+      authorize :invite, :create?
+
+      @invite      = Invite.new(resource_params)
+      @invite.user = current_user
+
+      if @invite.save
+        redirect_to admin_invites_path
+      else
+        @invites = Invite.page(params[:page])
+        render :index
+      end
+    end
+
+    def destroy
+      @invite = Invite.find(params[:id])
+      authorize @invite, :destroy?
+      @invite.expire!
+      redirect_to admin_invites_path
+    end
+
+    private
+
+    def resource_params
+      params.require(:invite).permit(:max_uses, :expires_in)
+    end
+
+    def filtered_invites
+      InviteFilter.new(filter_params).results
+    end
+
+    def filter_params
+      params.permit(:available, :expired)
+    end
+  end
+end
diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb
index 4f66ce708..535bd11d4 100644
--- a/app/controllers/admin/reported_statuses_controller.rb
+++ b/app/controllers/admin/reported_statuses_controller.rb
@@ -8,7 +8,7 @@ module Admin
     def create
       authorize :status, :update?
 
-      @form         = Form::StatusBatch.new(form_status_batch_params)
+      @form         = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
       flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
 
       redirect_to admin_report_path(@report)
@@ -16,13 +16,15 @@ module Admin
 
     def update
       authorize @status, :update?
-      @status.update(status_params)
+      @status.update!(status_params)
+      log_action :update, @status
       redirect_to admin_report_path(@report)
     end
 
     def destroy
       authorize @status, :destroy?
       RemovalWorker.perform_async(@status.id)
+      log_action :destroy, @status
       render json: @status
     end
 
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index 745757ee8..75db6b78a 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -25,12 +25,17 @@ module Admin
     def process_report
       case params[:outcome].to_s
       when 'resolve'
-        @report.update(action_taken_by_current_attributes)
+        @report.update!(action_taken_by_current_attributes)
+        log_action :resolve, @report
       when 'suspend'
         Admin::SuspensionWorker.perform_async(@report.target_account.id)
+        log_action :resolve, @report
+        log_action :suspend, @report.target_account
         resolve_all_target_account_reports
       when 'silence'
-        @report.target_account.update(silenced: true)
+        @report.target_account.update!(silenced: true)
+        log_action :resolve, @report
+        log_action :silence, @report.target_account
         resolve_all_target_account_reports
       else
         raise ActiveRecord::RecordNotFound
diff --git a/app/controllers/admin/resets_controller.rb b/app/controllers/admin/resets_controller.rb
index 00b590bf6..3e27d01ac 100644
--- a/app/controllers/admin/resets_controller.rb
+++ b/app/controllers/admin/resets_controller.rb
@@ -7,6 +7,7 @@ module Admin
     def create
       authorize @user, :reset_password?
       @user.send_reset_password_instructions
+      log_action :reset_password, @user
       redirect_to admin_accounts_path
     end
 
diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb
index 8f8685827..af7ec0740 100644
--- a/app/controllers/admin/roles_controller.rb
+++ b/app/controllers/admin/roles_controller.rb
@@ -7,12 +7,14 @@ module Admin
     def promote
       authorize @user, :promote?
       @user.promote!
+      log_action :promote, @user
       redirect_to admin_account_path(@user.account_id)
     end
 
     def demote
       authorize @user, :demote?
       @user.demote!
+      log_action :demote, @user
       redirect_to admin_account_path(@user.account_id)
     end
 
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index e81290228..eed5fb6b5 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -13,14 +13,17 @@ module Admin
       closed_registrations_message
       open_deletion
       timeline_preview
+      show_staff_badge
       bootstrap_timeline_accounts
       thumbnail
+      min_invite_role
     ).freeze
 
     BOOLEAN_SETTINGS = %w(
       open_registrations
       open_deletion
       timeline_preview
+      show_staff_badge
     ).freeze
 
     UPLOAD_SETTINGS = %w(
diff --git a/app/controllers/admin/silences_controller.rb b/app/controllers/admin/silences_controller.rb
index 01fb292de..4c06a9c0c 100644
--- a/app/controllers/admin/silences_controller.rb
+++ b/app/controllers/admin/silences_controller.rb
@@ -6,13 +6,15 @@ module Admin
 
     def create
       authorize @account, :silence?
-      @account.update(silenced: true)
+      @account.update!(silenced: true)
+      log_action :silence, @account
       redirect_to admin_accounts_path
     end
 
     def destroy
       authorize @account, :unsilence?
-      @account.update(silenced: false)
+      @account.update!(silenced: false)
+      log_action :unsilence, @account
       redirect_to admin_accounts_path
     end
 
diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb
index b54a9b824..5d4325f57 100644
--- a/app/controllers/admin/statuses_controller.rb
+++ b/app/controllers/admin/statuses_controller.rb
@@ -26,7 +26,7 @@ module Admin
     def create
       authorize :status, :update?
 
-      @form         = Form::StatusBatch.new(form_status_batch_params)
+      @form         = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
       flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
 
       redirect_to admin_account_statuses_path(@account.id, current_params)
@@ -34,13 +34,15 @@ module Admin
 
     def update
       authorize @status, :update?
-      @status.update(status_params)
+      @status.update!(status_params)
+      log_action :update, @status
       redirect_to admin_account_statuses_path(@account.id, current_params)
     end
 
     def destroy
       authorize @status, :destroy?
       RemovalWorker.perform_async(@status.id)
+      log_action :destroy, @status
       render json: @status
     end
 
diff --git a/app/controllers/admin/suspensions_controller.rb b/app/controllers/admin/suspensions_controller.rb
index 778feea5e..5f222e125 100644
--- a/app/controllers/admin/suspensions_controller.rb
+++ b/app/controllers/admin/suspensions_controller.rb
@@ -7,12 +7,14 @@ module Admin
     def create
       authorize @account, :suspend?
       Admin::SuspensionWorker.perform_async(@account.id)
+      log_action :suspend, @account
       redirect_to admin_accounts_path
     end
 
     def destroy
       authorize @account, :unsuspend?
       @account.unsuspend!
+      log_action :unsuspend, @account
       redirect_to admin_accounts_path
     end
 
diff --git a/app/controllers/admin/two_factor_authentications_controller.rb b/app/controllers/admin/two_factor_authentications_controller.rb
index 5a45d25cd..022107203 100644
--- a/app/controllers/admin/two_factor_authentications_controller.rb
+++ b/app/controllers/admin/two_factor_authentications_controller.rb
@@ -7,6 +7,7 @@ module Admin
     def destroy
       authorize @user, :disable_2fa?
       @user.disable_two_factor!
+      log_action :disable_2fa, @user
       redirect_to admin_accounts_path
     end
 
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 85eb2d60e..b1a2ed573 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -13,11 +13,9 @@ class Api::V1::AccountsController < Api::BaseController
   end
 
   def follow
-    reblogs_arg = { reblogs: params[:reblogs] }
-    
-    FollowService.new.call(current_user.account, @account.acct, reblogs_arg)
+    FollowService.new.call(current_user.account, @account.acct, reblogs: params[:reblogs])
 
-    options = @account.locked? ? {} : { following_map: { @account.id => reblogs_arg }, requested_map: { @account.id => false } }
+    options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: params[:reblogs] } }, requested_map: { @account.id => false } }
 
     render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
   end
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index 42e852c04..f4247fd95 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -17,13 +17,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController
 
   def build_resource(hash = nil)
     super(hash)
-    resource.locale = I18n.locale
+
+    resource.locale      = I18n.locale
+    resource.invite_code = params[:invite_code] if resource.invite_code.blank?
+
     resource.build_account if resource.account.nil?
   end
 
   def configure_sign_up_params
     devise_parameter_sanitizer.permit(:sign_up) do |u|
-      u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation)
+      u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code)
     end
   end
 
@@ -36,7 +39,19 @@ class Auth::RegistrationsController < Devise::RegistrationsController
   end
 
   def check_enabled_registrations
-    redirect_to root_path if single_user_mode? || !Setting.open_registrations
+    redirect_to root_path if single_user_mode? || !allowed_registrations?
+  end
+
+  def allowed_registrations?
+    Setting.open_registrations || (invite_code.present? && Invite.find_by(code: invite_code)&.valid_for_use?)
+  end
+
+  def invite_code
+    if params[:user]
+      params[:user][:invite_code]
+    else
+      params[:invite_code]
+    end
   end
 
   private
diff --git a/app/controllers/concerns/accountable_concern.rb b/app/controllers/concerns/accountable_concern.rb
new file mode 100644
index 000000000..3cdcffc51
--- /dev/null
+++ b/app/controllers/concerns/accountable_concern.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module AccountableConcern
+  extend ActiveSupport::Concern
+
+  def log_action(action, target)
+    Admin::ActionLog.create(account: current_account, action: action, target: target)
+  end
+end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
new file mode 100644
index 000000000..38d6c8d73
--- /dev/null
+++ b/app/controllers/invites_controller.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class InvitesController < ApplicationController
+  include Authorization
+
+  layout 'admin'
+
+  before_action :authenticate_user!
+
+  def index
+    authorize :invite, :create?
+
+    @invites = Invite.where(user: current_user)
+    @invite  = Invite.new(expires_in: 1.day.to_i)
+  end
+
+  def create
+    authorize :invite, :create?
+
+    @invite      = Invite.new(resource_params)
+    @invite.user = current_user
+
+    if @invite.save
+      redirect_to invites_path
+    else
+      @invites = Invite.where(user: current_user)
+      render :index
+    end
+  end
+
+  def destroy
+    @invite = Invite.where(user: current_user).find(params[:id])
+    authorize @invite, :destroy?
+    @invite.expire!
+    redirect_to invites_path
+  end
+
+  private
+
+  def resource_params
+    params.require(:invite).permit(:max_uses, :expires_in)
+  end
+end
diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb
new file mode 100644
index 000000000..b18403a7f
--- /dev/null
+++ b/app/controllers/settings/migrations_controller.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class Settings::MigrationsController < ApplicationController
+  layout 'admin'
+
+  before_action :authenticate_user!
+
+  def show
+    @migration = Form::Migration.new(account: current_account.moved_to_account)
+  end
+
+  def update
+    @migration = Form::Migration.new(resource_params)
+
+    if @migration.valid? && migration_account_changed?
+      current_account.update!(moved_to_account: @migration.account)
+      ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
+      redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg')
+    else
+      render :show
+    end
+  end
+
+  private
+
+  def resource_params
+    params.require(:migration).permit(:acct)
+  end
+
+  def migration_account_changed?
+    current_account.moved_to_account_id != @migration.account&.id
+  end
+end
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
new file mode 100644
index 000000000..e85243e57
--- /dev/null
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+module Admin::ActionLogsHelper
+  def log_target(log)
+    if log.target
+      linkable_log_target(log.target)
+    else
+      log_target_from_history(log.target_type, log.recorded_changes)
+    end
+  end
+
+  def linkable_log_target(record)
+    case record.class.name
+    when 'Account'
+      link_to record.acct, admin_account_path(record.id)
+    when 'User'
+      link_to record.account.acct, admin_account_path(record.account_id)
+    when 'CustomEmoji'
+      record.shortcode
+    when 'Report'
+      link_to "##{record.id}", admin_report_path(record)
+    when 'DomainBlock', 'EmailDomainBlock'
+      link_to record.domain, "https://#{record.domain}"
+    when 'Status'
+      link_to record.account.acct, TagManager.instance.url_for(record)
+    end
+  end
+
+  def log_target_from_history(type, attributes)
+    case type
+    when 'CustomEmoji'
+      attributes['shortcode']
+    when 'DomainBlock', 'EmailDomainBlock'
+      link_to attributes['domain'], "https://#{attributes['domain']}"
+    when 'Status'
+      tmp_status = Status.new(attributes)
+      link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
+    end
+  end
+
+  def relevant_log_changes(log)
+    if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
+      log.recorded_changes.slice('domain')
+    elsif log.target_type == 'CustomEmoji' && log.action == :update
+      log.recorded_changes.slice('domain', 'visible_in_picker')
+    elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
+      log.recorded_changes.slice('moderator', 'admin')
+    elsif log.target_type == 'DomainBlock'
+      log.recorded_changes.slice('severity', 'reject_media')
+    elsif log.target_type == 'Status' && log.action == :update
+      log.recorded_changes.slice('sensitive')
+    end
+  end
+
+  def log_extra_attributes(hash)
+    safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ')
+  end
+
+  def log_change(val)
+    return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array)
+    safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→')
+  end
+
+  def icon_for_log(log)
+    case log.target_type
+    when 'Account', 'User'
+      'user'
+    when 'CustomEmoji'
+      'file'
+    when 'Report'
+      'flag'
+    when 'DomainBlock'
+      'lock'
+    when 'EmailDomainBlock'
+      'envelope'
+    when 'Status'
+      'pencil'
+    end
+  end
+
+  def class_for_log_icon(log)
+    case log.action
+    when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
+      'positive'
+    when :create
+      opposite_verbs?(log) ? 'negative' : 'positive'
+    when :update, :reset_password, :disable_2fa, :memorialize
+      'neutral'
+    when :demote, :silence, :disable, :suspend
+      'negative'
+    when :destroy
+      opposite_verbs?(log) ? 'positive' : 'negative'
+    else
+      ''
+    end
+  end
+
+  private
+
+  def opposite_verbs?(log)
+    %w(DomainBlock EmailDomainBlock).include?(log.target_type)
+  end
+end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index e0fae9d9a..73250cbf5 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -3,8 +3,9 @@
 module Admin::FilterHelper
   ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze
   REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
+  INVITE_FILTER = %i(available expired).freeze
 
-  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
+  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER
 
   def filter_link_to(text, link_to_params, link_class_params = link_to_params)
     new_url = filtered_url_for(link_to_params)
diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb
index a3441e6f9..6c7c38070 100644
--- a/app/helpers/jsonld_helper.rb
+++ b/app/helpers/jsonld_helper.rb
@@ -9,6 +9,24 @@ module JsonLdHelper
     value.is_a?(Array) ? value.first : value
   end
 
+  # The url attribute can be a string, an array of strings, or an array of objects.
+  # The objects could include a mimeType. Not-included mimeType means it's text/html.
+  def url_to_href(value, preferred_type = nil)
+    single_value = if value.is_a?(Array) && !value.first.is_a?(String)
+                     value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
+                   elsif value.is_a?(Array)
+                     value.first
+                   else
+                     value
+                   end
+
+    if single_value.nil? || single_value.is_a?(String)
+      single_value
+    else
+      single_value['href']
+    end
+  end
+
   def as_array(value)
     value.is_a?(Array) ? value : [value]
   end
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 6662285d0..b0d9e3757 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -9,6 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
 import { HotKeys } from 'react-hotkeys';
 import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
+import classNames from 'classnames';
 
 // We use the component (and not the container) since we do not want
 // to use the progress bar to show download progress
@@ -21,6 +22,7 @@ export default class Status extends ImmutablePureComponent {
   };
 
   static propTypes = {
+    containerId: PropTypes.string,
     id: PropTypes.string,
     status: ImmutablePropTypes.map,
     account: ImmutablePropTypes.map,
@@ -59,6 +61,7 @@ export default class Status extends ImmutablePureComponent {
     'muted',
     'collapse',
     'notification',
+    'hidden',
   ]
 
   updateOnStates = [
@@ -187,7 +190,9 @@ export default class Status extends ImmutablePureComponent {
   }
 
   handleExpandedToggle = () => {
-    this.setExpansion(this.state.isExpanded || !this.props.status.get('spoiler') ? null : true);
+    if (this.props.status.get('spoiler_text')) {
+      this.setExpansion(this.state.isExpanded ? null : true);
+    }
   };
 
   handleOpenVideo = startTime => {
@@ -221,11 +226,11 @@ export default class Status extends ImmutablePureComponent {
   }
 
   handleHotkeyMoveUp = () => {
-    this.props.onMoveUp(this.props.status.get('id'));
+    this.props.onMoveUp(this.props.containerId || this.props.id);
   }
 
   handleHotkeyMoveDown = () => {
-    this.props.onMoveDown(this.props.status.get('id'));
+    this.props.onMoveDown(this.props.containerId || this.props.id);
   }
 
   handleRef = c => {
@@ -370,31 +375,24 @@ export default class Status extends ImmutablePureComponent {
       openProfile: this.handleHotkeyOpenProfile,
       moveUp: this.handleHotkeyMoveUp,
       moveDown: this.handleHotkeyMoveDown,
+      toggleSpoiler: this.handleExpandedToggle,
     };
 
+    const computedClass = classNames('status', `status-${status.get('visibility')}`, {
+      collapsed: isExpanded === false,
+      'has-background': isExpanded === false && background,
+      'marked-for-delete': this.state.markedForDelete,
+      muted,
+    }, 'focusable');
+
     return (
       <HotKeys handlers={handlers}>
         <div
-          className={
-            `status${
-              muted ? ' muted' : ''
-            } status-${status.get('visibility')}${
-              isExpanded === false ? ' collapsed' : ''
-            }${
-              isExpanded === false && background ? ' has-background' : ''
-            }${
-              this.state.markedForDelete ? ' marked-for-delete' : ''
-            }`
-          }
-          style={{
-            backgroundImage: (
-              isExpanded === false && background ?
-              `url(${background})` :
-              'none'
-            ),
-          }}
+          className={computedClass}
+          style={isExpanded === false && background ? { backgroundImage: `url(${background})` } : null}
           {...selectorAttribs}
           ref={handleRef}
+          tabIndex='0'
         >
           {prepend && account ? (
             <StatusPrepend
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index 1a4f35278..b753de7b3 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -45,6 +45,7 @@ const makeMapStateToProps = () => {
     }
 
     return {
+      containerId : props.containerId || props.id,  //  Should match reblogStatus's id for reblogs
       status      : status,
       account     : account || props.account,
       settings    : state.get('local_settings'),
diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow.js b/app/javascript/flavours/glitch/features/notifications/components/follow.js
index 4b682733e..54506f67c 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/follow.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/follow.js
@@ -14,6 +14,7 @@ import NotificationOverlayContainer from '../containers/overlay_container';
 export default class NotificationFollow extends ImmutablePureComponent {
 
   static propTypes = {
+    hidden: PropTypes.bool,
     id: PropTypes.string.isRequired,
     account: ImmutablePropTypes.map.isRequired,
     notification: ImmutablePropTypes.map.isRequired,
@@ -57,7 +58,7 @@ export default class NotificationFollow extends ImmutablePureComponent {
   }
 
   render () {
-    const { account, notification } = this.props;
+    const { account, notification, hidden } = this.props;
 
     //  Links to the display name.
     const displayName = account.get('display_name_html') || account.get('username');
@@ -87,7 +88,7 @@ export default class NotificationFollow extends ImmutablePureComponent {
             />
           </div>
 
-          <AccountContainer id={account.get('id')} withNote={false} />
+          <AccountContainer hidden={hidden} id={account.get('id')} withNote={false} />
           <NotificationOverlayContainer notification={notification} />
         </div>
       </HotKeys>
diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js
index 185da8395..cc77426d3 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/notification.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js
@@ -16,70 +16,75 @@ export default class Notification extends ImmutablePureComponent {
     onMoveUp: PropTypes.func.isRequired,
     onMoveDown: PropTypes.func.isRequired,
     onMention: PropTypes.func.isRequired,
-    settings: ImmutablePropTypes.map.isRequired,
   };
 
-  renderFollow () {
-    const { notification } = this.props;
-    return (
-      <NotificationFollow
-        id={notification.get('id')}
-        account={notification.get('account')}
-        notification={notification}
-      />
-    );
-  }
-
-  renderMention () {
-    const { notification } = this.props;
-    return (
-      <StatusContainer
-        id={notification.get('status')}
-        notification={notification}
-        withDismiss
-      />
-    );
-  }
-
-  renderFavourite () {
-    const { notification } = this.props;
-    return (
-      <StatusContainer
-        id={notification.get('status')}
-        account={notification.get('account')}
-        prepend='favourite'
-        muted
-        notification={notification}
-        withDismiss
-      />
-    );
-  }
-
-  renderReblog () {
-    const { notification } = this.props;
-    return (
-      <StatusContainer
-        id={notification.get('status')}
-        account={notification.get('account')}
-        prepend='reblog'
-        muted
-        notification={notification}
-        withDismiss
-      />
-    );
-  }
-
   render () {
-    const { notification } = this.props;
+    const {
+      hidden,
+      notification,
+      onMoveDown,
+      onMoveUp,
+      onMention,
+    } = this.props;
+
     switch(notification.get('type')) {
     case 'follow':
-      return this.renderFollow();
+      return (
+        <NotificationFollow
+          hidden={hidden}
+          id={notification.get('id')}
+          account={notification.get('account')}
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+        />
+      );
     case 'mention':
-      return this.renderMention();
+      return (
+        <StatusContainer
+          containerId={notification.get('id')}
+          hidden={hidden}
+          id={notification.get('status')}
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          withDismiss
+        />
+      );
     case 'favourite':
-      return this.renderFavourite();
+      return (
+        <StatusContainer
+          containerId={notification.get('id')}
+          hidden={hidden}
+          id={notification.get('status')}
+          account={notification.get('account')}
+          prepend='favourite'
+          muted
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          withDismiss
+        />
+      );
     case 'reblog':
-      return this.renderReblog();
+      return (
+        <StatusContainer
+          containerId={notification.get('id')}
+          hidden={hidden}
+          id={notification.get('status')}
+          account={notification.get('account')}
+          prepend='reblog'
+          muted
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          withDismiss
+        />
+      );
     default:
       return null;
     }
diff --git a/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
index c60e72e1c..be007f30b 100644
--- a/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
+++ b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
@@ -11,7 +11,6 @@ const makeMapStateToProps = () => {
 
   const mapStateToProps = (state, props) => ({
     notification: getNotification(state, props.notification, props.accountId),
-    settings: state.get('local_settings'),
     notifCleaning: state.getIn(['notifications', 'cleaningMode']),
   });
 
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index 7c6f436d6..0cb5238b0 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -41,7 +41,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
 
   render () {
     const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
-    const { settings } = this.props;
+    const { expanded, setExpansion, settings } = this.props;
 
     let media           = '';
     let mediaIcon       = null;
@@ -109,6 +109,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
           status={status}
           media={media}
           mediaIcon={mediaIcon}
+          expanded={expanded}
+          setExpansion={setExpansion}
         />
 
         <div className='detailed-status__meta'>
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index 73b212bba..93b0fe9d9 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -71,6 +71,7 @@ export default class Status extends ImmutablePureComponent {
 
   state = {
     fullscreen: false,
+    isExpanded: null,
   };
 
   componentWillMount () {
@@ -88,6 +89,12 @@ export default class Status extends ImmutablePureComponent {
     }
   }
 
+  handleExpandedToggle = () => {
+    if (this.props.status.get('spoiler_text')) {
+      this.setExpansion(this.state.isExpanded ? null : true);
+    }
+  };
+
   handleFavouriteClick = (status) => {
     if (status.get('favourited')) {
       this.props.dispatch(unfavourite(status));
@@ -241,6 +248,10 @@ export default class Status extends ImmutablePureComponent {
     ));
   }
 
+  setExpansion = value => {
+    this.setState({ isExpanded: value ? true : null });
+  }
+
   setRef = c => {
     this.node = c;
   }
@@ -272,8 +283,9 @@ export default class Status extends ImmutablePureComponent {
 
   render () {
     let ancestors, descendants;
+    const { setExpansion } = this;
     const { status, settings, ancestorsIds, descendantsIds } = this.props;
-    const { fullscreen } = this.state;
+    const { fullscreen, isExpanded } = this.state;
 
     if (status === null) {
       return (
@@ -300,6 +312,7 @@ export default class Status extends ImmutablePureComponent {
       boost: this.handleHotkeyBoost,
       mention: this.handleHotkeyMention,
       openProfile: this.handleHotkeyOpenProfile,
+      toggleSpoiler: this.handleExpandedToggle,
     };
 
     return (
@@ -317,6 +330,8 @@ export default class Status extends ImmutablePureComponent {
                   settings={settings}
                   onOpenVideo={this.handleOpenVideo}
                   onOpenMedia={this.handleOpenMedia}
+                  expanded={isExpanded}
+                  setExpansion={setExpansion}
                 />
 
                 <ActionBar
diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index 620630faf..4a1982916 100644
--- a/app/javascript/flavours/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -84,6 +84,7 @@ const keyMap = {
   goToProfile: 'g u',
   goToBlocked: 'g b',
   goToMuted: 'g m',
+  toggleSpoiler: 'x',
 };
 
 @connect(mapStateToProps)
diff --git a/app/javascript/flavours/glitch/styles/_mixins.scss b/app/javascript/flavours/glitch/styles/_mixins.scss
index 79a8149fd..102723e39 100644
--- a/app/javascript/flavours/glitch/styles/_mixins.scss
+++ b/app/javascript/flavours/glitch/styles/_mixins.scss
@@ -46,6 +46,7 @@
     margin-left: -22px;
     margin-right: -22px;
     width: inherit;
+    max-width: none;
     height: 250px;
   }
 }
diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss
index 3be8db4b4..ade8df018 100644
--- a/app/javascript/flavours/glitch/styles/components.scss
+++ b/app/javascript/flavours/glitch/styles/components.scss
@@ -1058,6 +1058,14 @@
     color: $ui-secondary-color;
   }
 
+  .account__avatar {
+    @include avatar-radius();
+    @include avatar-size(90px);
+    display: block;
+    margin: 0 auto 10px;
+    overflow: hidden;
+  }
+
   .account__header__display-name {
     color: $primary-text-color;
     display: inline-block;
@@ -1248,14 +1256,6 @@
   }
 }
 
-.account__header__avatar {
-  @include avatar-radius();
-  @include avatar-size(90px);
-  display: block;
-  margin: 0 auto 10px;
-  overflow: hidden;
-}
-
 .account-authorize {
   padding: 14px 10px;
 
diff --git a/app/javascript/glitch/locales/pl.json b/app/javascript/glitch/locales/pl.json
new file mode 100644
index 000000000..1481b6a2a
--- /dev/null
+++ b/app/javascript/glitch/locales/pl.json
@@ -0,0 +1,44 @@
+{
+  "getting_started.open_source_notice": "Glitchsoc jest wolnym i otwartoźródłowym forkiem oprogramowania {Mastodon}. Możesz współtworzyć projekt lub zgłaszać błędy na GitHubie pod adresem {github}.",
+  "layout.auto": "Automatyczny",
+  "layout.current_is": "Twój obecny układ to:",
+  "layout.desktop": "Desktopowy",
+  "layout.mobile": "Mobilny",
+  "navigation_bar.app_settings": "Ustawienia aplikacji",
+  "getting_started.onboarding": "Rozejrzyj się",
+  "onboarding.page_one.federation": "{domain} jest 'instancją' Mastodona. Mastodon to sieć działających niezależnie serwerów tworzących jedną sieć społecznościową. Te serwery nazywane są instancjami.",
+  "onboarding.page_one.welcome": "Witamy na {domain}!",
+  "onboarding.page_six.github": "{domain} jest oparty na Glitchsoc. Glitchsoc jest {forkiem} {Mastodon}a kompatybilnym z każdym klientem i aplikacją Mastodona. Glitchsoc jest całkowicie wolnym i otwartoźródłowym oprogramowaniem. Możesz zgłaszać błędy i sugestie funkcji oraz współtworzyć projekt na {github}.",
+  "settings.auto_collapse": "Automatyczne zwijanie",
+  "settings.auto_collapse_all": "Wszystko",
+  "settings.auto_collapse_lengthy": "Długie wpisy",
+  "settings.auto_collapse_media": "Wpisy z zawartością multimedialną",
+  "settings.auto_collapse_notifications": "Powiadomienia",
+  "settings.auto_collapse_reblogs": "Podbicia",
+  "settings.auto_collapse_replies": "Odpowiedzi",
+  "settings.close": "Zamknij",
+  "settings.collapsed_statuses": "Zwijanie wpisów",
+  "settings.enable_collapsed": "Włącz zwijanie wpisów",
+  "settings.general": "Ogólne",
+  "settings.image_backgrounds": "Obrazy w tle",
+  "settings.image_backgrounds_media": "Wyświetlaj zawartość multimedialną zwiniętych wpisów",
+  "settings.image_backgrounds_users": "Nadaj tło zwiniętym wpisom",
+  "settings.media": "Zawartość multimedialna",
+  "settings.media_letterbox": "Letterbox media",
+  "settings.media_fullwidth": "Podgląd zawartości multimedialnej o pełnej szerokości",
+  "settings.preferences": "Preferencje użyytkownika",
+  "settings.wide_view": "Szeroki widok (tylko w trybie desktopowym)",
+  "settings.navbar_under": "Pasek nawigacji na dole (tylko w trybie mobilnym)",
+  "status.collapse": "Zwiń",
+  "status.uncollapse": "Rozwiń",
+
+  "notification.markForDeletion": "Oznacz do usunięcia",
+  "notifications.clear": "Wyczyść wszystkie powiadomienia",
+  "notifications.marked_clear_confirmation": "Czy na pewno chcesz bezpowrtonie usunąć wszystkie powiadomienia?",
+  "notifications.marked_clear": "Usuń zaznaczone powiadomienia",
+
+  "notification_purge.btn_all": "Zaznacz\nwszystkie",
+  "notification_purge.btn_none": "Odznacz\nwszystkie",
+  "notification_purge.btn_invert": "Odwróć\nzaznaczenie",
+  "notification_purge.btn_apply": "Usuń\nzaznaczone"
+}
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index fbaebf786..f63325658 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -105,12 +105,13 @@ export function fetchAccountFail(id, error) {
   };
 };
 
-export function followAccount(id) {
+export function followAccount(id, reblogs = true) {
   return (dispatch, getState) => {
+    const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
     dispatch(followAccountRequest(id));
 
-    api(getState).post(`/api/v1/accounts/${id}/follow`).then(response => {
-      dispatch(followAccountSuccess(response.data));
+    api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
+      dispatch(followAccountSuccess(response.data, alreadyFollowing));
     }).catch(error => {
       dispatch(followAccountFail(error));
     });
@@ -136,10 +137,11 @@ export function followAccountRequest(id) {
   };
 };
 
-export function followAccountSuccess(relationship) {
+export function followAccountSuccess(relationship, alreadyFollowing) {
   return {
     type: ACCOUNT_FOLLOW_SUCCESS,
     relationship,
+    alreadyFollowing,
   };
 };
 
diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js
new file mode 100644
index 000000000..332e42166
--- /dev/null
+++ b/app/javascript/mastodon/actions/lists.js
@@ -0,0 +1,28 @@
+import api from '../api';
+
+export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
+export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
+export const LIST_FETCH_FAIL    = 'LIST_FETCH_FAIL';
+
+export const fetchList = id => (dispatch, getState) => {
+  dispatch(fetchListRequest(id));
+
+  api(getState).get(`/api/v1/lists/${id}`)
+    .then(({ data }) => dispatch(fetchListSuccess(data)))
+    .catch(err => dispatch(fetchListFail(err)));
+};
+
+export const fetchListRequest = id => ({
+  type: LIST_FETCH_REQUEST,
+  id,
+});
+
+export const fetchListSuccess = list => ({
+  type: LIST_FETCH_SUCCESS,
+  list,
+});
+
+export const fetchListFail = error => ({
+  type: LIST_FETCH_FAIL,
+  error,
+});
diff --git a/app/javascript/mastodon/actions/mutes.js b/app/javascript/mastodon/actions/mutes.js
index 3474250fe..daa76a8f7 100644
--- a/app/javascript/mastodon/actions/mutes.js
+++ b/app/javascript/mastodon/actions/mutes.js
@@ -1,6 +1,6 @@
 import api, { getLinks } from '../api';
 import { fetchRelationships } from './accounts';
-import { openModal } from '../../mastodon/actions/modal';
+import { openModal } from './modal';
 
 export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
 export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
@@ -100,4 +100,4 @@ export function toggleHideNotifications() {
   return dispatch => {
     dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
   };
-}
\ No newline at end of file
+}
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index dcce048ca..c22152edd 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -51,3 +51,4 @@ export const connectCommunityStream = () => connectTimelineStream('community', '
 export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
 export const connectPublicStream = () => connectTimelineStream('public', 'public');
 export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
+export const connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`);
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 09abe2702..f8843d1d9 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -118,6 +118,7 @@ export const refreshCommunityTimeline    = () => refreshTimeline('community', '/
 export const refreshAccountTimeline      = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
 export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
 export const refreshHashtagTimeline      = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
+export const refreshListTimeline         = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
 
 export function refreshTimelineFail(timeline, error, skipLoading) {
   return {
@@ -158,6 +159,7 @@ export const expandCommunityTimeline    = () => expandTimeline('community', '/ap
 export const expandAccountTimeline      = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
 export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
 export const expandHashtagTimeline      = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
+export const expandListTimeline         = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
 
 export function expandTimelineRequest(timeline) {
   return {
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index 724b10980..1f2d7690f 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -93,7 +93,7 @@ export default class Account extends ImmutablePureComponent {
           </div>
         );
       } else {
-        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
+        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following ? true : false} />;
       }
     }
 
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index 3a3ebf487..43dc0d6e3 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -110,7 +110,7 @@ export default class Dropdown extends React.PureComponent {
     icon: PropTypes.string.isRequired,
     items: PropTypes.array.isRequired,
     size: PropTypes.number.isRequired,
-    ariaLabel: PropTypes.string,
+    title: PropTypes.string,
     disabled: PropTypes.bool,
     status: ImmutablePropTypes.map,
     isUserTouching: PropTypes.func,
@@ -120,7 +120,7 @@ export default class Dropdown extends React.PureComponent {
   };
 
   static defaultProps = {
-    ariaLabel: 'Menu',
+    title: 'Menu',
   };
 
   state = {
@@ -186,14 +186,14 @@ export default class Dropdown extends React.PureComponent {
   }
 
   render () {
-    const { icon, items, size, ariaLabel, disabled } = this.props;
+    const { icon, items, size, title, disabled } = this.props;
     const { expanded } = this.state;
 
     return (
       <div onKeyDown={this.handleKeyDown}>
         <IconButton
           icon={icon}
-          title={ariaLabel}
+          title={title}
           active={expanded}
           disabled={disabled}
           size={size}
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index 7021c198e..cd59c7845 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -179,7 +179,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
         {shareButton}
 
         <div className='status__action-bar-dropdown'>
-          <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
+          <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js
index e375131d4..389296c42 100644
--- a/app/javascript/mastodon/features/account/components/action_bar.js
+++ b/app/javascript/mastodon/features/account/components/action_bar.js
@@ -20,6 +20,8 @@ const messages = defineMessages({
   media: { id: 'account.media', defaultMessage: 'Media' },
   blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
   unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+  hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
+  showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
 });
 
 @injectIntl
@@ -30,6 +32,7 @@ export default class ActionBar extends React.PureComponent {
     onFollow: PropTypes.func,
     onBlock: PropTypes.func.isRequired,
     onMention: PropTypes.func.isRequired,
+    onReblogToggle: PropTypes.func.isRequired,
     onReport: PropTypes.func.isRequired,
     onMute: PropTypes.func.isRequired,
     onBlockDomain: PropTypes.func.isRequired,
@@ -60,6 +63,15 @@ export default class ActionBar extends React.PureComponent {
     if (account.get('id') === me) {
       menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
     } else {
+      const following = account.getIn(['relationship', 'following']);
+      if (following) {
+        if (following.get('reblogs')) {
+          menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
+        } else {
+          menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
+        }
+      }
+
       if (account.getIn(['relationship', 'muting'])) {
         menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
       } else {
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index f0d2d481f..b2399ae9b 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -7,6 +7,7 @@ import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { autoPlayGif, me } from '../../../initial_state';
+import classNames from 'classnames';
 
 const messages = defineMessages({
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@@ -102,6 +103,10 @@ export default class Header extends ImmutablePureComponent {
       }
     }
 
+    if (account.get('moved')) {
+      actionBtn = '';
+    }
+
     if (account.get('locked')) {
       lockedIcon = <i className='fa fa-lock' />;
     }
@@ -110,7 +115,7 @@ export default class Header extends ImmutablePureComponent {
     const displayNameHtml = { __html: account.get('display_name_html') };
 
     return (
-      <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
+      <div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${account.get('header')})` }}>
         <div>
           <Avatar account={account} />
 
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js
index 8cf7b92ca..0ddb6b6c1 100644
--- a/app/javascript/mastodon/features/account_timeline/components/header.js
+++ b/app/javascript/mastodon/features/account_timeline/components/header.js
@@ -5,6 +5,7 @@ import InnerHeader from '../../account/components/header';
 import ActionBar from '../../account/components/action_bar';
 import MissingIndicator from '../../../components/missing_indicator';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import MovedNote from './moved_note';
 
 export default class Header extends ImmutablePureComponent {
 
@@ -13,6 +14,7 @@ export default class Header extends ImmutablePureComponent {
     onFollow: PropTypes.func.isRequired,
     onBlock: PropTypes.func.isRequired,
     onMention: PropTypes.func.isRequired,
+    onReblogToggle: PropTypes.func.isRequired,
     onReport: PropTypes.func.isRequired,
     onMute: PropTypes.func.isRequired,
     onBlockDomain: PropTypes.func.isRequired,
@@ -39,6 +41,10 @@ export default class Header extends ImmutablePureComponent {
     this.props.onReport(this.props.account);
   }
 
+  handleReblogToggle = () => {
+    this.props.onReblogToggle(this.props.account);
+  }
+
   handleMute = () => {
     this.props.onMute(this.props.account);
   }
@@ -68,6 +74,8 @@ export default class Header extends ImmutablePureComponent {
 
     return (
       <div className='account-timeline__header'>
+        {account.get('moved') && <MovedNote from={account} to={account.get('moved')} />}
+
         <InnerHeader
           account={account}
           onFollow={this.handleFollow}
@@ -77,6 +85,7 @@ export default class Header extends ImmutablePureComponent {
           account={account}
           onBlock={this.handleBlock}
           onMention={this.handleMention}
+          onReblogToggle={this.handleReblogToggle}
           onReport={this.handleReport}
           onMute={this.handleMute}
           onBlockDomain={this.handleBlockDomain}
diff --git a/app/javascript/mastodon/features/account_timeline/components/moved_note.js b/app/javascript/mastodon/features/account_timeline/components/moved_note.js
new file mode 100644
index 000000000..1c0e081cc
--- /dev/null
+++ b/app/javascript/mastodon/features/account_timeline/components/moved_note.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { FormattedMessage } from 'react-intl';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import AvatarOverlay from '../../../components/avatar_overlay';
+import DisplayName from '../../../components/display_name';
+
+export default class MovedNote extends ImmutablePureComponent {
+
+  static contextTypes = {
+    router: PropTypes.object,
+  };
+
+  static propTypes = {
+    from: ImmutablePropTypes.map.isRequired,
+    to: ImmutablePropTypes.map.isRequired,
+  };
+
+  handleAccountClick = e => {
+    if (e.button === 0) {
+      e.preventDefault();
+      this.context.router.history.push(`/accounts/${this.props.to.get('id')}`);
+    }
+
+    e.stopPropagation();
+  }
+
+  render () {
+    const { from, to } = this.props;
+    const displayNameHtml = { __html: from.get('display_name_html') };
+
+    return (
+      <div className='account__moved-note'>
+        <div className='account__moved-note__message'>
+          <div className='account__moved-note__icon-wrapper'><i className='fa fa-fw fa-suitcase account__moved-note__icon' /></div>
+          <FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <strong dangerouslySetInnerHTML={displayNameHtml} /> }} />
+        </div>
+
+        <a href={to.get('url')} onClick={this.handleAccountClick} className='detailed-status__display-name'>
+          <div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
+          <DisplayName account={to} />
+        </a>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index 8e50ec405..b41eb19d4 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -67,6 +67,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     dispatch(mentionCompose(account, router));
   },
 
+  onReblogToggle (account) {
+    if (account.getIn(['relationship', 'following', 'reblogs'])) {
+      dispatch(followAccount(account.get('id'), false));
+    } else {
+      dispatch(followAccount(account.get('id'), true));
+    }
+  },
+
   onReport (account) {
     dispatch(initReport(account));
   },
diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js
index 7f346854c..3014c4033 100644
--- a/app/javascript/mastodon/features/compose/components/navigation_bar.js
+++ b/app/javascript/mastodon/features/compose/components/navigation_bar.js
@@ -11,7 +11,7 @@ export default class NavigationBar extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map.isRequired,
-    onClose: PropTypes.func.isRequired,
+    onClose: PropTypes.func,
   };
 
   render () {
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index 4b4ae6947..df4284207 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -25,6 +25,7 @@ const messages = defineMessages({
   mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
   info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
   pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
+  keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
 });
 
 const mapStateToProps = state => ({
@@ -78,6 +79,7 @@ export default class GettingStarted extends ImmutablePureComponent {
     navItems = navItems.concat([
       <ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
       <ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
+      <ColumnLink key='9' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' hideOnMobile />,
     ]);
 
     return (
diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.js b/app/javascript/mastodon/features/keyboard_shortcuts/index.js
new file mode 100644
index 000000000..22991fcba
--- /dev/null
+++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.js
@@ -0,0 +1,98 @@
+import React from 'react';
+import Column from '../ui/components/column';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import PropTypes from 'prop-types';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+const messages = defineMessages({
+  heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' },
+});
+
+@injectIntl
+export default class KeyboardShortcuts extends ImmutablePureComponent {
+
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
+  };
+
+  render () {
+    const { intl } = this.props;
+
+    return (
+      <Column icon='question' heading={intl.formatMessage(messages.heading)}>
+        <ColumnBackButtonSlim />
+        <div className='keyboard-shortcuts scrollable optionally-scrollable'>
+          <table>
+            <thead>
+              <tr>
+                <th><FormattedMessage id='keyboard_shortcuts.hotkey' defaultMessage='Hotkey' /></th>
+                <th><FormattedMessage id='keyboard_shortcuts.description' defaultMessage='Description' /></th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr>
+                <td><code>r</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.reply' defaultMessage='to reply' /></td>
+              </tr>
+              <tr>
+                <td><code>m</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.mention' defaultMessage='to mention author' /></td>
+              </tr>
+              <tr>
+                <td><code>f</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to favourite' /></td>
+              </tr>
+              <tr>
+                <td><code>b</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to boost' /></td>
+              </tr>
+              <tr>
+                <td><code>enter</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td>
+              </tr>
+              <tr>
+                <td><code>up</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td>
+              </tr>
+              <tr>
+                <td><code>down</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' /></td>
+              </tr>
+              <tr>
+                <td><code>1</code>-<code>9</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.column' defaultMessage='to focus a status in one of the columns' /></td>
+              </tr>
+              <tr>
+                <td><code>n</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.compose' defaultMessage='to focus the compose textarea' /></td>
+              </tr>
+              <tr>
+                <td><code>alt</code>+<code>n</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a brand new toot' /></td>
+              </tr>
+              <tr>
+                <td><code>backspace</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td>
+              </tr>
+              <tr>
+                <td><code>s</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></td>
+              </tr>
+              <tr>
+                <td><code>esc</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.unfocus' defaultMessage='to un-focus compose textarea/search' /></td>
+              </tr>
+              <tr>
+                <td><code>?</code></td>
+                <td><FormattedMessage id='keyboard_shortcuts.legend' defaultMessage='to display this legend' /></td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </Column>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
new file mode 100644
index 000000000..71f6e36a8
--- /dev/null
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -0,0 +1,106 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import StatusListContainer from '../ui/containers/status_list_container';
+import Column from '../../components/column';
+import ColumnHeader from '../../components/column_header';
+import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
+import { FormattedMessage } from 'react-intl';
+import { connectListStream } from '../../actions/streaming';
+import { refreshListTimeline, expandListTimeline } from '../../actions/timelines';
+import { fetchList } from '../../actions/lists';
+
+const mapStateToProps = (state, props) => ({
+  list: state.getIn(['lists', props.params.id]),
+  hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
+});
+
+@connect(mapStateToProps)
+export default class ListTimeline extends React.PureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    columnId: PropTypes.string,
+    hasUnread: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    list: ImmutablePropTypes.map,
+  };
+
+  handlePin = () => {
+    const { columnId, dispatch } = this.props;
+
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('LIST', { id: this.props.params.id }));
+    }
+  }
+
+  handleMove = (dir) => {
+    const { columnId, dispatch } = this.props;
+    dispatch(moveColumn(columnId, dir));
+  }
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  }
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+    const { id } = this.props.params;
+
+    dispatch(fetchList(id));
+    dispatch(refreshListTimeline(id));
+
+    this.disconnect = dispatch(connectListStream(id));
+  }
+
+  componentWillUnmount () {
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
+    }
+  }
+
+  setRef = c => {
+    this.column = c;
+  }
+
+  handleLoadMore = () => {
+    const { id } = this.props.params;
+    this.props.dispatch(expandListTimeline(id));
+  }
+
+  render () {
+    const { hasUnread, columnId, multiColumn, list } = this.props;
+    const { id } = this.props.params;
+    const pinned = !!columnId;
+    const title  = list ? list.get('title') : id;
+
+    return (
+      <Column ref={this.setRef}>
+        <ColumnHeader
+          icon='bars'
+          active={hasUnread}
+          title={title}
+          onPin={this.handlePin}
+          onMove={this.handleMove}
+          onClick={this.handleHeaderClick}
+          pinned={pinned}
+          multiColumn={multiColumn}
+        />
+
+        <StatusListContainer
+          trackScroll={!pinned}
+          scrollKey={`list_timeline-${columnId}`}
+          timelineId={`list:${id}`}
+          loadMore={this.handleLoadMore}
+          emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />}
+        />
+      </Column>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
index 7b65420d0..99834df6c 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.js
+++ b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -120,7 +120,7 @@ export default class ActionBar extends React.PureComponent {
         {shareButton}
 
         <div className='detailed-status__action-bar-dropdown'>
-          <DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' ariaLabel='More' />
+          <DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' title='More' />
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js
index bb83374b9..680bf63ab 100644
--- a/app/javascript/mastodon/features/status/components/card.js
+++ b/app/javascript/mastodon/features/status/components/card.js
@@ -1,5 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import Immutable from 'immutable';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import punycode from 'punycode';
 import classnames from 'classnames';
@@ -24,6 +25,7 @@ export default class Card extends React.PureComponent {
   static propTypes = {
     card: ImmutablePropTypes.map,
     maxDescription: PropTypes.number,
+    onOpenMedia: PropTypes.func.isRequired,
   };
 
   static defaultProps = {
@@ -34,6 +36,27 @@ export default class Card extends React.PureComponent {
     width: 0,
   };
 
+  handlePhotoClick = () => {
+    const { card, onOpenMedia } = this.props;
+
+    onOpenMedia(
+      Immutable.fromJS([
+        {
+          type: 'image',
+          url: card.get('url'),
+          description: card.get('title'),
+          meta: {
+            original: {
+              width: card.get('width'),
+              height: card.get('height'),
+            },
+          },
+        },
+      ]),
+      0
+    );
+  };
+
   renderLink () {
     const { card, maxDescription } = this.props;
 
@@ -73,9 +96,16 @@ export default class Card extends React.PureComponent {
     const { card } = this.props;
 
     return (
-      <a href={card.get('url')} className='status-card-photo' target='_blank' rel='noopener'>
-        <img src={card.get('url')} alt={card.get('title')} width={card.get('width')} height={card.get('height')} />
-      </a>
+      <img
+        className='status-card-photo'
+        onClick={this.handlePhotoClick}
+        role='button'
+        tabIndex='0'
+        src={card.get('url')}
+        alt={card.get('title')}
+        width={card.get('width')}
+        height={card.get('height')}
+      />
     );
   }
 
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 81f71749b..abdb9a3f6 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -73,7 +73,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
         );
       }
     } else if (status.get('spoiler_text').length === 0) {
-      media = <CardContainer statusId={status.get('id')} />;
+      media = <CardContainer onOpenMedia={this.props.onOpenMedia} statusId={status.get('id')} />;
     }
 
     if (status.get('application')) {
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 5610095b9..93ed9e605 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -11,7 +11,7 @@ import BundleContainer from '../containers/bundle_container';
 import ColumnLoading from './column_loading';
 import DrawerLoading from './drawer_loading';
 import BundleColumnError from './bundle_column_error';
-import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
+import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components';
 
 import detectPassiveEvents from 'detect-passive-events';
 import { scrollRight } from '../../../scroll';
@@ -24,6 +24,7 @@ const componentMap = {
   'COMMUNITY': CommunityTimeline,
   'HASHTAG': HashtagTimeline,
   'FAVOURITES': FavouritedStatuses,
+  'LIST': ListTimeline,
 };
 
 @component => injectIntl(component, { withRef: true })
diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js
index aad594380..e3e7197c5 100644
--- a/app/javascript/mastodon/features/ui/components/image_loader.js
+++ b/app/javascript/mastodon/features/ui/components/image_loader.js
@@ -7,7 +7,7 @@ export default class ImageLoader extends React.PureComponent {
   static propTypes = {
     alt: PropTypes.string,
     src: PropTypes.string.isRequired,
-    previewSrc: PropTypes.string.isRequired,
+    previewSrc: PropTypes.string,
     width: PropTypes.number,
     height: PropTypes.number,
   }
@@ -47,7 +47,7 @@ export default class ImageLoader extends React.PureComponent {
     this.removeEventListeners();
     this.setState({ loading: true, error: false });
     Promise.all([
-      this.loadPreviewCanvas(props),
+      props.previewSrc && this.loadPreviewCanvas(props),
       this.hasSize() && this.loadOriginalImage(props),
     ].filter(Boolean))
       .then(() => {
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js
index f41a83089..02591a51f 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.js
@@ -92,7 +92,7 @@ export default class MediaModal extends ImmutablePureComponent {
       const height = image.getIn(['meta', 'original', 'height']) || null;
 
       if (image.get('type') === 'image') {
-        return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} alt={image.get('description')} key={image.get('preview_url')} />;
+        return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} alt={image.get('description')} key={image.get('url')} />;
       } else if (image.get('type') === 'gifv') {
         return <ExtendedVideoPlayer src={image.get('url')} muted controls={false} width={width} height={height} key={image.get('preview_url')} alt={image.get('description')} />;
       }
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index f28b37099..361326961 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -19,6 +19,7 @@ import {
   Compose,
   Status,
   GettingStarted,
+  KeyboardShortcuts,
   PublicTimeline,
   CommunityTimeline,
   AccountTimeline,
@@ -33,6 +34,7 @@ import {
   FollowRequests,
   GenericNotFound,
   FavouritedStatuses,
+  ListTimeline,
   Blocks,
   Mutes,
   PinnedStatuses,
@@ -55,6 +57,7 @@ const mapStateToProps = state => ({
 });
 
 const keyMap = {
+  help: '?',
   new: 'n',
   search: 's',
   forceNew: 'option+n',
@@ -297,6 +300,14 @@ export default class UI extends React.Component {
     this.hotkeys = c;
   }
 
+  handleHotkeyToggleHelp = () => {
+    if (this.props.location.pathname === '/keyboard-shortcuts') {
+      this.context.router.history.goBack();
+    } else {
+      this.context.router.history.push('/keyboard-shortcuts');
+    }
+  }
+
   handleHotkeyGoToHome = () => {
     this.context.router.history.push('/timelines/home');
   }
@@ -342,6 +353,7 @@ export default class UI extends React.Component {
     const { children } = this.props;
 
     const handlers = {
+      help: this.handleHotkeyToggleHelp,
       new: this.handleHotkeyNew,
       search: this.handleHotkeySearch,
       forceNew: this.handleHotkeyForceNew,
@@ -368,10 +380,12 @@ export default class UI extends React.Component {
             <WrappedSwitch>
               <Redirect from='/' to='/getting-started' exact />
               <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
+              <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
               <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
               <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
               <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
               <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
+              <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} />
 
               <WrappedRoute path='/notifications' component={Notifications} content={children} />
               <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 39663d5ca..b741f668e 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -26,6 +26,10 @@ export function HashtagTimeline () {
   return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
 }
 
+export function ListTimeline () {
+  return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
+}
+
 export function Status () {
   return import(/* webpackChunkName: "features/status" */'../../status');
 }
@@ -34,6 +38,10 @@ export function GettingStarted () {
   return import(/* webpackChunkName: "features/getting_started" */'../../getting_started');
 }
 
+export function KeyboardShortcuts () {
+  return import(/* webpackChunkName: "features/keyboard_shortcuts" */'../../keyboard_shortcuts');
+}
+
 export function PinnedStatuses () {
   return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses');
 }
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 7cc8ea237..596a519bd 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -7,17 +7,22 @@
   "account.followers": "المتابعون",
   "account.follows": "يتبع",
   "account.follows_you": "يتابعك",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "وسائط",
   "account.mention": "أُذكُر @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "أكتم @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "المشاركات",
   "account.report": "أبلغ عن @{name}",
   "account.requested": "في انتظار الموافقة",
   "account.share": "مشاركة @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "إلغاء الحظر عن @{name}",
   "account.unblock_domain": "فك حظر {domain}",
   "account.unfollow": "إلغاء المتابعة",
   "account.unmute": "إلغاء الكتم عن @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "عرض الملف الشخصي كاملا",
   "boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة",
   "bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
   "empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.",
   "empty_column.home.public_timeline": "الخيط العام",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
   "empty_column.public": "لا يوجد شيء هنا ! قم بتحرير شيء ما بشكل عام، أو اتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام.",
   "follow_request.authorize": "ترخيص",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "عرض الترقيات",
   "home.column_settings.show_replies": "عرض الردود",
   "home.settings": "إعدادات العمود",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "إغلاق",
   "lightbox.next": "التالي",
   "lightbox.previous": "العودة",
   "loading_indicator.label": "تحميل ...",
   "media_gallery.toggle_visible": "عرض / إخفاء",
   "missing_indicator.label": "تعذر العثور عليه",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "الحسابات المحجوبة",
   "navigation_bar.community_timeline": "الخيط العام المحلي",
   "navigation_bar.edit_profile": "تعديل الملف الشخصي",
   "navigation_bar.favourites": "المفضلة",
   "navigation_bar.follow_requests": "طلبات المتابعة",
   "navigation_bar.info": "معلومات إضافية",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "خروج",
   "navigation_bar.mutes": "الحسابات المكتومة",
   "navigation_bar.pins": "التبويقات المثبتة",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "الرئيسية",
   "tabs_bar.local_timeline": "المحلي",
   "tabs_bar.notifications": "الإخطارات",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "إسحب ثم أفلت للرفع",
   "upload_button.label": "إضافة وسائط",
   "upload_form.description": "وصف للمعاقين بصريا",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index da2372cff..82f092648 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -7,17 +7,22 @@
   "account.followers": "Последователи",
   "account.follows": "Следвам",
   "account.follows_you": "Твой последовател",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Споменаване",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Mute @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Публикации",
   "account.report": "Report @{name}",
   "account.requested": "В очакване на одобрение",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Не блокирай",
   "account.unblock_domain": "Unhide {domain}",
   "account.unfollow": "Не следвай",
   "account.unmute": "Unmute @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "You can press {combo} to skip this next time",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.public_timeline": "the public timeline",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
   "follow_request.authorize": "Authorize",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Show boosts",
   "home.column_settings.show_replies": "Show replies",
   "home.settings": "Column settings",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Затвори",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Зареждане...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Blocked users",
   "navigation_bar.community_timeline": "Local timeline",
   "navigation_bar.edit_profile": "Редактирай профил",
   "navigation_bar.favourites": "Favourites",
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "Extended information",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Излизане",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Начало",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Известия",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Добави медия",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index af732921d..baa9432de 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -7,17 +7,22 @@
   "account.followers": "Seguidors",
   "account.follows": "Seguint",
   "account.follows_you": "et segueix",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Esmentar @{name}",
+  "account.moved_to": "{name} s'ha mogut a:",
   "account.mute": "Silenciar @{name}",
+  "account.mute_notifications": "Notificacions desactivades de @{name}",
   "account.posts": "Publicacions",
   "account.report": "Informe @{name}",
-  "account.requested": "Esperant aprovació",
+  "account.requested": "Esperant aprovació. Clic per a cancel·lar la petició de seguiment",
   "account.share": "Compartir el perfil de @{name}",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Desbloquejar @{name}",
   "account.unblock_domain": "Mostra {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Treure silenci de @{name}",
+  "account.unmute_notifications": "Activar notificacions de @{name}",
   "account.view_full_profile": "Veure el perfil complet",
   "boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop",
   "bundle_column_error.body": "S'ha produït un error en carregar aquest component.",
@@ -51,7 +56,7 @@
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "Marcar multimèdia com a sensible",
   "compose_form.spoiler": "Amagar text darrera l'advertència",
-  "compose_form.spoiler_placeholder": "Advertència de contingut",
+  "compose_form.spoiler_placeholder": "Escriu l'advertència aquí",
   "confirmation_modal.cancel": "Cancel·lar",
   "confirmations.block.confirm": "Bloquejar",
   "confirmations.block.message": "Estàs segur que vols bloquejar {name}?",
@@ -64,7 +69,7 @@
   "confirmations.unfollow.confirm": "Deixar de seguir",
   "confirmations.unfollow.message": "Estàs segur que vols deixar de seguir {name}?",
   "embed.instructions": "Incrusta aquest estat al lloc web copiant el codi a continuació.",
-  "embed.preview": "A continuació s'explica com:",
+  "embed.preview": "Aquí tenim quin aspecte tindrá:",
   "emoji_button.activity": "Activitat",
   "emoji_button.custom": "Personalitzat",
   "emoji_button.flags": "Flags",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.",
   "empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
   "empty_column.home.public_timeline": "la línia de temps pública",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.",
   "empty_column.public": "No hi ha res aquí! Escriu alguna cosa públicament o segueix manualment usuaris d'altres instàncies per omplir-ho",
   "follow_request.authorize": "Autoritzar",
@@ -95,21 +101,40 @@
   "home.column_settings.advanced": "Avançat",
   "home.column_settings.basic": "Bàsic",
   "home.column_settings.filter_regex": "Filtrar per expressió regular",
-  "home.column_settings.show_reblogs": "Mostrar 'boosts'",
+  "home.column_settings.show_reblogs": "Mostrar impulsos",
   "home.column_settings.show_replies": "Mostrar respostes",
   "home.settings": "Ajustos de columna",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Tancar",
   "lightbox.next": "Següent",
   "lightbox.previous": "Anterior",
   "loading_indicator.label": "Carregant...",
   "media_gallery.toggle_visible": "Alternar visibilitat",
   "missing_indicator.label": "No trobat",
+  "mute_modal.hide_notifications": "Amagar notificacions d'aquest usuari?",
   "navigation_bar.blocks": "Usuaris bloquejats",
   "navigation_bar.community_timeline": "Línia de temps Local",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favorits",
   "navigation_bar.follow_requests": "Sol·licituds de seguiment",
   "navigation_bar.info": "Informació addicional",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Tancar sessió",
   "navigation_bar.mutes": "Usuaris silenciats",
   "navigation_bar.pins": "Toots fixats",
@@ -127,7 +152,7 @@
   "notifications.column_settings.mention": "Mencions:",
   "notifications.column_settings.push": "Push notificacions",
   "notifications.column_settings.push_meta": "Aquest dispositiu",
-  "notifications.column_settings.reblog": "Boosts:",
+  "notifications.column_settings.reblog": "Impulsos:",
   "notifications.column_settings.show": "Mostrar en la columna",
   "notifications.column_settings.sound": "Reproduïr so",
   "onboarding.done": "Fet",
@@ -159,11 +184,11 @@
   "privacy.public.short": "Públic",
   "privacy.unlisted.long": "No publicar en línies de temps públiques",
   "privacy.unlisted.short": "No llistat",
-  "relative_time.days": "fa {number} jorns",
+  "relative_time.days": "fa {number} dies",
   "relative_time.hours": "fa {number} hores",
   "relative_time.just_now": "ara",
-  "relative_time.minutes": "fa {number} minutes",
-  "relative_time.seconds": "fa {number} segondes",
+  "relative_time.minutes": "fa {number} minuts",
+  "relative_time.seconds": "fa {number} segons",
   "reply_indicator.cancel": "Cancel·lar",
   "report.placeholder": "Comentaris addicionals",
   "report.submit": "Enviar",
@@ -187,7 +212,7 @@
   "status.mute_conversation": "Silenciar conversació",
   "status.open": "Ampliar aquest estat",
   "status.pin": "Fixat en el perfil",
-  "status.reblog": "Boost",
+  "status.reblog": "Impuls",
   "status.reblogged_by": "{name} ha retootejat",
   "status.reply": "Respondre",
   "status.replyAll": "Respondre al tema",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Inici",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificacions",
+  "ui.beforeunload": "El vostre esborrany es perdrà si sortiu de Mastodon.",
   "upload_area.title": "Arrossega i deixa anar per carregar",
   "upload_button.label": "Afegir multimèdia",
   "upload_form.description": "Descriure els problemes visuals",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 283a2946f..343528899 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -7,17 +7,22 @@
   "account.followers": "Folgende",
   "account.follows": "Folgt",
   "account.follows_you": "Folgt dir",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Medien",
   "account.mention": "@{name} erwähnen",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "@{name} stummschalten",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Beiträge",
   "account.report": "@{name} melden",
   "account.requested": "Warte auf Erlaubnis. Klicke zum Abbrechen",
   "account.share": "Profil von @{name} teilen",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "@{name} entblocken",
   "account.unblock_domain": "{domain} wieder anzeigen",
   "account.unfollow": "Entfolgen",
   "account.unmute": "@{name} nicht mehr stummschalten",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "Vollständiges Profil anzeigen",
   "boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
   "bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Unter diesem Hashtag gibt es noch nichts.",
   "empty_column.home": "Deine Startseite ist leer! Besuche {public} oder nutze die Suche, um loszulegen und andere Leute zu finden.",
   "empty_column.home.public_timeline": "die öffentliche Zeitleiste",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um ins Gespräch zu kommen.",
   "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Instanzen, um die Zeitleiste aufzufüllen",
   "follow_request.authorize": "Erlauben",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
   "home.column_settings.show_replies": "Antworten anzeigen",
   "home.settings": "Spalteneinstellungen",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Schließen",
   "lightbox.next": "Weiter",
   "lightbox.previous": "Zurück",
   "loading_indicator.label": "Wird geladen …",
   "media_gallery.toggle_visible": "Sichtbarkeit umschalten",
   "missing_indicator.label": "Nicht gefunden",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Blockierte Profile",
   "navigation_bar.community_timeline": "Lokale Zeitleiste",
   "navigation_bar.edit_profile": "Profil bearbeiten",
   "navigation_bar.favourites": "Favoriten",
   "navigation_bar.follow_requests": "Folgeanfragen",
   "navigation_bar.info": "Über diese Instanz",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Abmelden",
   "navigation_bar.mutes": "Stummgeschaltete Profile",
   "navigation_bar.pins": "Angeheftete Beiträge",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Startseite",
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Mitteilungen",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Zum Hochladen hereinziehen",
   "upload_button.label": "Mediendatei hinzufügen",
   "upload_form.description": "Für Menschen mit Sehbehinderung beschreiben",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index f400b283f..be751589e 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -29,6 +29,14 @@
       {
         "defaultMessage": "Unmute @{name}",
         "id": "account.unmute"
+      },
+      {
+        "defaultMessage": "Mute notifications from @{name}",
+        "id": "account.mute_notifications"
+      },
+      {
+        "defaultMessage": "Unmute notifications from @{name}",
+        "id": "account.unmute_notifications"
       }
     ],
     "path": "app/javascript/mastodon/components/account.json"
@@ -284,16 +292,8 @@
         "id": "confirmations.block.confirm"
       },
       {
-        "defaultMessage": "Mute",
-        "id": "confirmations.mute.confirm"
-      },
-      {
         "defaultMessage": "Are you sure you want to block {name}?",
         "id": "confirmations.block.message"
-      },
-      {
-        "defaultMessage": "Are you sure you want to mute {name}?",
-        "id": "confirmations.mute.message"
       }
     ],
     "path": "app/javascript/mastodon/containers/status_container.json"
@@ -310,6 +310,15 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "{name} has moved to:",
+        "id": "account.moved_to"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/account_timeline/components/moved_note.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Unfollow",
         "id": "confirmations.unfollow.confirm"
       },
@@ -318,10 +327,6 @@
         "id": "confirmations.block.confirm"
       },
       {
-        "defaultMessage": "Mute",
-        "id": "confirmations.mute.confirm"
-      },
-      {
         "defaultMessage": "Hide entire domain",
         "id": "confirmations.domain_block.confirm"
       },
@@ -334,10 +339,6 @@
         "id": "confirmations.block.message"
       },
       {
-        "defaultMessage": "Are you sure you want to mute {name}?",
-        "id": "confirmations.mute.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.",
         "id": "confirmations.domain_block.message"
       }
@@ -399,6 +400,14 @@
         "id": "account.unblock_domain"
       },
       {
+        "defaultMessage": "Hide boosts from @{name}",
+        "id": "account.hide_reblogs"
+      },
+      {
+        "defaultMessage": "Show boosts from @{name}",
+        "id": "account.show_reblogs"
+      },
+      {
         "defaultMessage": "Information below may reflect the user's profile incompletely.",
         "id": "account.disclaimer_full"
       },
@@ -849,6 +858,10 @@
         "id": "navigation_bar.pins"
       },
       {
+        "defaultMessage": "Keyboard shortcuts",
+        "id": "navigation_bar.keyboard_shortcuts"
+      },
+      {
         "defaultMessage": "FAQ",
         "id": "getting_started.faq"
       },
@@ -925,6 +938,88 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Keyboard Shortcuts",
+        "id": "keyboard_shortcuts.heading"
+      },
+      {
+        "defaultMessage": "Hotkey",
+        "id": "keyboard_shortcuts.hotkey"
+      },
+      {
+        "defaultMessage": "Description",
+        "id": "keyboard_shortcuts.description"
+      },
+      {
+        "defaultMessage": "to reply",
+        "id": "keyboard_shortcuts.reply"
+      },
+      {
+        "defaultMessage": "to mention author",
+        "id": "keyboard_shortcuts.mention"
+      },
+      {
+        "defaultMessage": "to favourite",
+        "id": "keyboard_shortcuts.favourite"
+      },
+      {
+        "defaultMessage": "to boost",
+        "id": "keyboard_shortcuts.boost"
+      },
+      {
+        "defaultMessage": "to open status",
+        "id": "keyboard_shortcuts.enter"
+      },
+      {
+        "defaultMessage": "to move up in the list",
+        "id": "keyboard_shortcuts.up"
+      },
+      {
+        "defaultMessage": "to move down in the list",
+        "id": "keyboard_shortcuts.down"
+      },
+      {
+        "defaultMessage": "to focus a status in one of the columns",
+        "id": "keyboard_shortcuts.column"
+      },
+      {
+        "defaultMessage": "to focus the compose textarea",
+        "id": "keyboard_shortcuts.compose"
+      },
+      {
+        "defaultMessage": "to start a brand new toot",
+        "id": "keyboard_shortcuts.toot"
+      },
+      {
+        "defaultMessage": "to navigate back",
+        "id": "keyboard_shortcuts.back"
+      },
+      {
+        "defaultMessage": "to focus search",
+        "id": "keyboard_shortcuts.search"
+      },
+      {
+        "defaultMessage": "to un-focus compose textarea/search",
+        "id": "keyboard_shortcuts.unfocus"
+      },
+      {
+        "defaultMessage": "to display this legend",
+        "id": "keyboard_shortcuts.legend"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/keyboard_shortcuts/index.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "There is nothing in this list yet.",
+        "id": "empty_column.list"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/list_timeline/index.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Muted users",
         "id": "column.mutes"
       }
@@ -1210,6 +1305,27 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Are you sure you want to mute {name}?",
+        "id": "confirmations.mute.message"
+      },
+      {
+        "defaultMessage": "Hide notifications from this user?",
+        "id": "mute_modal.hide_notifications"
+      },
+      {
+        "defaultMessage": "Cancel",
+        "id": "confirmation_modal.cancel"
+      },
+      {
+        "defaultMessage": "Mute",
+        "id": "confirmations.mute.confirm"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/ui/components/mute_modal.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Home",
         "id": "column.home"
       },
@@ -1362,6 +1478,15 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Your draft will be lost if you leave Mastodon.",
+        "id": "ui.beforeunload"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/ui/index.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Play",
         "id": "video.play"
       },
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 1d0bbcee5..4e0b838b0 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -7,17 +7,22 @@
   "account.followers": "Followers",
   "account.follows": "Follows",
   "account.follows_you": "Follows you",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Mention @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Mute @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Posts",
   "account.report": "Report @{name}",
   "account.requested": "Awaiting approval. Click to cancel follow request",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Unblock @{name}",
   "account.unblock_domain": "Unhide {domain}",
   "account.unfollow": "Unfollow",
   "account.unmute": "Unmute @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "You can press {combo} to skip this next time",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.public_timeline": "the public timeline",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
   "follow_request.authorize": "Authorize",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Show boosts",
   "home.column_settings.show_replies": "Show replies",
   "home.settings": "Column settings",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Close",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Loading...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Blocked users",
   "navigation_bar.community_timeline": "Local timeline",
   "navigation_bar.edit_profile": "Edit profile",
   "navigation_bar.favourites": "Favourites",
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "About this instance",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Home",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 3f67a8fff..f9e32ba5e 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -7,17 +7,22 @@
   "account.followers": "Sekvantoj",
   "account.follows": "Sekvatoj",
   "account.follows_you": "Sekvas vin",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Sonbildaĵoj",
   "account.mention": "Mencii @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Silentigi @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Mesaĝoj",
   "account.report": "Signali @{name}",
   "account.requested": "Atendas aprobon",
   "account.share": "Diskonigi la profilon de @{name}",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Malbloki @{name}",
   "account.unblock_domain": "Malkaŝi {domain}",
   "account.unfollow": "Ne plus sekvi",
   "account.unmute": "Malsilentigi @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "Vidi plenan profilon",
   "boost_modal.combo": "La proksiman fojon, premu {combo} por pasigi",
   "bundle_column_error.body": "Io malfunkciis ŝargante tiun ĉi komponanton.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Ĝise, neniu enhavo estas asociita kun tiu kradvorto.",
   "empty_column.home": "Via hejma tempolinio estas malplena! Vizitu {public} aŭ uzu la serĉilon por renkonti aliajn uzantojn.",
   "empty_column.home.public_timeline": "la publika tempolinio",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Vi dume ne havas sciigojn. Interagi kun aliajn uzantojn por komenci la konversacion.",
   "empty_column.public": "Estas nenio ĉi tie! Publike skribu ion, aŭ mane sekvu uzantojn de aliaj instancoj por plenigi la publikan tempolinion.",
   "follow_request.authorize": "Akcepti",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Montri diskonigojn",
   "home.column_settings.show_replies": "Montri respondojn",
   "home.settings": "Agordoj de la kolumno",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Fermi",
   "lightbox.next": "Malantaŭa",
   "lightbox.previous": "Antaŭa",
   "loading_indicator.label": "Ŝarganta…",
   "media_gallery.toggle_visible": "Baskuli videblecon",
   "missing_indicator.label": "Ne trovita",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Blokitaj uzantoj",
   "navigation_bar.community_timeline": "Loka tempolinio",
   "navigation_bar.edit_profile": "Redakti la profilon",
   "navigation_bar.favourites": "Favoritaj",
   "navigation_bar.follow_requests": "Abonpetoj",
   "navigation_bar.info": "Plia informo",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Elsaluti",
   "navigation_bar.mutes": "Silentigitaj uzantoj",
   "navigation_bar.pins": "Alpinglitaj pepoj",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Hejmo",
   "tabs_bar.local_timeline": "Loka tempolinio",
   "tabs_bar.notifications": "Sciigoj",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Algliti por alŝuti",
   "upload_button.label": "Aldoni sonbildaĵon",
   "upload_form.description": "Priskribi por la misvidantaj",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 6e8e94700..38ba291e4 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -7,17 +7,22 @@
   "account.followers": "Seguidores",
   "account.follows": "Sigue",
   "account.follows_you": "Te sigue",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Mencionar a @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Silenciar a @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Publicaciones",
   "account.report": "Reportar a @{name}",
   "account.requested": "Esperando aprobación",
   "account.share": "Compartir el perfil de @{name}",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Desbloquear a @{name}",
   "account.unblock_domain": "Mostrar a {domain}",
   "account.unfollow": "Dejar de seguir",
   "account.unmute": "Dejar de silenciar a @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "Ver perfil completo",
   "boost_modal.combo": "Puedes presionar {combo} para saltear este aviso la próxima vez",
   "bundle_column_error.body": "Algo salió mal al cargar este componente.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "No hay nada en este hashtag aún.",
   "empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
   "empty_column.home.public_timeline": "la línea de tiempo pública",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.",
   "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo.",
   "follow_request.authorize": "Autorizar",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Mostrar retoots",
   "home.column_settings.show_replies": "Mostrar respuestas",
   "home.settings": "Ajustes de columna",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Cerrar",
   "lightbox.next": "Siguiente",
   "lightbox.previous": "Anterior",
   "loading_indicator.label": "Cargando…",
   "media_gallery.toggle_visible": "Cambiar visibilidad",
   "missing_indicator.label": "No encontrado",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Usuarios bloqueados",
   "navigation_bar.community_timeline": "Historia local",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritos",
   "navigation_bar.follow_requests": "Solicitudes para seguirte",
   "navigation_bar.info": "Información adicional",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Cerrar sesión",
   "navigation_bar.mutes": "Usuarios silenciados",
   "navigation_bar.pins": "Toots fijados",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Inicio",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificaciones",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Arrastra y suelta para subir",
   "upload_button.label": "Subir multimedia",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 995d1b5ae..dafb52bb5 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -7,17 +7,22 @@
   "account.followers": "پیگیران",
   "account.follows": "پی می‌گیرد",
   "account.follows_you": "پیگیر شماست",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "رسانه",
   "account.mention": "نام‌بردن از @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "بی‌صدا کردن @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "نوشته‌ها",
   "account.report": "گزارش @{name}",
   "account.requested": "در انتظار پذیرش",
   "account.share": "هم‌رسانی نمایهٔ @{name}",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "رفع انسداد @{name}",
   "account.unblock_domain": "رفع پنهان‌سازی از {domain}",
   "account.unfollow": "پایان پیگیری",
   "account.unmute": "باصدا کردن @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "نمایش نمایهٔ کامل",
   "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
   "bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "هنوز هیچ چیزی با این هشتگ نیست.",
   "empty_column.home": "شما هنوز پیگیر کسی نیستید. {public} را ببینید یا چیزی را جستجو کنید تا کاربران دیگر را ببینید.",
   "empty_column.home.public_timeline": "فهرست نوشته‌های همه‌جا",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "هنوز هیچ اعلانی ندارید. به نوشته‌های دیگران واکنش نشان دهید تا گفتگو آغاز شود.",
   "empty_column.public": "این‌جا هنوز چیزی نیست! خودتان چیزی بنویسید یا کاربران دیگر را پی بگیرید تا این‌جا پر شود",
   "follow_request.authorize": "اجازه دهید",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "نمایش بازبوق‌ها",
   "home.column_settings.show_replies": "نمایش پاسخ‌ها",
   "home.settings": "تنظیمات ستون",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "بستن",
   "lightbox.next": "بعدی",
   "lightbox.previous": "قبلی",
   "loading_indicator.label": "بارگیری...",
   "media_gallery.toggle_visible": "تغییر پیدایی",
   "missing_indicator.label": "پیدا نشد",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "کاربران مسدودشده",
   "navigation_bar.community_timeline": "نوشته‌های محلی",
   "navigation_bar.edit_profile": "ویرایش نمایه",
   "navigation_bar.favourites": "پسندیده‌ها",
   "navigation_bar.follow_requests": "درخواست‌های پیگیری",
   "navigation_bar.info": "اطلاعات تکمیلی",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "خروج",
   "navigation_bar.mutes": "کاربران بی‌صداشده",
   "navigation_bar.pins": "نوشته‌های ثابت",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "خانه",
   "tabs_bar.local_timeline": "محلی",
   "tabs_bar.notifications": "اعلان‌ها",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "برای بارگذاری به این‌جا بکشید",
   "upload_button.label": "افزودن تصویر",
   "upload_form.description": "نوشتهٔ توضیحی برای کم‌بینایان و نابینایان",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index af08be5d1..e3739eb68 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -7,17 +7,22 @@
   "account.followers": "Seuraajia",
   "account.follows": "Seuraa",
   "account.follows_you": "Seuraa sinua",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Mainitse @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Mute @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Postit",
   "account.report": "Report @{name}",
   "account.requested": "Odottaa hyväksyntää",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Salli @{name}",
   "account.unblock_domain": "Unhide {domain}",
   "account.unfollow": "Lopeta seuraaminen",
   "account.unmute": "Unmute @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "You can press {combo} to skip this next time",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.public_timeline": "the public timeline",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
   "follow_request.authorize": "Authorize",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Show boosts",
   "home.column_settings.show_replies": "Show replies",
   "home.settings": "Column settings",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Sulje",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Ladataan...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Blocked users",
   "navigation_bar.community_timeline": "Paikallinen aikajana",
   "navigation_bar.edit_profile": "Muokkaa profiilia",
   "navigation_bar.favourites": "Favourites",
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "Extended information",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Kirjaudu ulos",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Koti",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Ilmoitukset",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Lisää mediaa",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 219bf4da1..e4cabf52c 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -7,17 +7,22 @@
   "account.followers": "Abonné⋅e⋅s",
   "account.follows": "Abonnements",
   "account.follows_you": "Vous suit",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Média",
   "account.mention": "Mentionner",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Masquer",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Statuts",
   "account.report": "Signaler",
   "account.requested": "Invitation envoyée",
   "account.share": "Partager le profil de @{name}",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Débloquer",
   "account.unblock_domain": "Ne plus masquer {domain}",
   "account.unfollow": "Ne plus suivre",
   "account.unmute": "Ne plus masquer",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "Afficher le profil complet",
   "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
   "bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag",
   "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.",
   "empty_column.home.public_timeline": "le fil public",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateur⋅ice⋅s pour débuter la conversation.",
   "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.",
   "follow_request.authorize": "Accepter",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Afficher les partages",
   "home.column_settings.show_replies": "Afficher les réponses",
   "home.settings": "Paramètres de la colonne",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Fermer",
   "lightbox.next": "Suivant",
   "lightbox.previous": "Précédent",
   "loading_indicator.label": "Chargement…",
   "media_gallery.toggle_visible": "Modifier la visibilité",
   "missing_indicator.label": "Non trouvé",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Comptes bloqués",
   "navigation_bar.community_timeline": "Fil public local",
   "navigation_bar.edit_profile": "Modifier le profil",
   "navigation_bar.favourites": "Favoris",
   "navigation_bar.follow_requests": "Demandes de suivi",
   "navigation_bar.info": "Plus d’informations",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Déconnexion",
   "navigation_bar.mutes": "Comptes masqués",
   "navigation_bar.pins": "Pouets épinglés",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Accueil",
   "tabs_bar.local_timeline": "Fil public local",
   "tabs_bar.notifications": "Notifications",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Glissez et déposez pour envoyer",
   "upload_button.label": "Joindre un média",
   "upload_form.description": "Décrire pour les malvoyants",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index a260f0968..fdebdf92a 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -7,17 +7,22 @@
   "account.followers": "עוקבים",
   "account.follows": "נעקבים",
   "account.follows_you": "במעקב אחריך",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "מדיה",
   "account.mention": "אזכור של @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "להשתיק את @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "הודעות",
   "account.report": "לדווח על @{name}",
   "account.requested": "בהמתנה לאישור",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "הסרת חסימה מעל @{name}",
   "account.unblock_domain": "הסר חסימה מקהילת {domain}",
   "account.unfollow": "הפסקת מעקב",
   "account.unmute": "הפסקת השתקת @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "אין כלום בהאשתג הזה עדיין.",
   "empty_column.home": "אף אחד לא במעקב עדיין. אפשר לבקר ב{public} או להשתמש בחיפוש כדי להתחיל ולהכיר חצוצרנים אחרים.",
   "empty_column.home.public_timeline": "ציר זמן בין-קהילתי",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב!",
   "empty_column.public": "אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות.",
   "follow_request.authorize": "קבלה",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "הצגת הדהודים",
   "home.column_settings.show_replies": "הצגת תגובות",
   "home.settings": "הגדרות טור",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "סגירה",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "טוען...",
   "media_gallery.toggle_visible": "נראה\\בלתי נראה",
   "missing_indicator.label": "לא נמצא",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "חסימות",
   "navigation_bar.community_timeline": "ציר זמן מקומי",
   "navigation_bar.edit_profile": "עריכת פרופיל",
   "navigation_bar.favourites": "חיבובים",
   "navigation_bar.follow_requests": "בקשות מעקב",
   "navigation_bar.info": "מידע נוסף",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "יציאה",
   "navigation_bar.mutes": "השתקות",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "בבית",
   "tabs_bar.local_timeline": "ציר זמן מקומי",
   "tabs_bar.notifications": "התראות",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "ניתן להעלות על ידי Drag & drop",
   "upload_button.label": "הוספת מדיה",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index 6ac7fc3b4..497917b9c 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -7,17 +7,22 @@
   "account.followers": "Sljedbenici",
   "account.follows": "Slijedi",
   "account.follows_you": "te slijedi",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Spomeni @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Utišaj @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Postovi",
   "account.report": "Prijavi @{name}",
   "account.requested": "Čeka pristanak",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Deblokiraj @{name}",
   "account.unblock_domain": "Poništi sakrivanje {domain}",
   "account.unfollow": "Prestani slijediti",
   "account.unmute": "Poništi utišavanje @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.",
   "empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.",
   "empty_column.home.public_timeline": "javni timeline",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.",
   "empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio",
   "follow_request.authorize": "Autoriziraj",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Pokaži boostove",
   "home.column_settings.show_replies": "Pokaži odgovore",
   "home.settings": "Postavke Stupca",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Zatvori",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Učitavam...",
   "media_gallery.toggle_visible": "Preklopi vidljivost",
   "missing_indicator.label": "Nije nađen",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Blokirani korisnici",
   "navigation_bar.community_timeline": "Lokalni timeline",
   "navigation_bar.edit_profile": "Uredi profil",
   "navigation_bar.favourites": "Favoriti",
   "navigation_bar.follow_requests": "Zahtjevi za slijeđenje",
   "navigation_bar.info": "Više informacija",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Odjavi se",
   "navigation_bar.mutes": "Utišani korisnici",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Dom",
   "tabs_bar.local_timeline": "Lokalno",
   "tabs_bar.notifications": "Notifikacije",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Povuci i spusti kako bi uploadao",
   "upload_button.label": "Dodaj media",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 5892e606e..6df09aea9 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -7,17 +7,22 @@
   "account.followers": "Követők",
   "account.follows": "Követve",
   "account.follows_you": "Követnek téged",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Említés",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Mute @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Posts",
   "account.report": "Report @{name}",
   "account.requested": "Awaiting approval",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Blokkolás levétele",
   "account.unblock_domain": "Unhide {domain}",
   "account.unfollow": "Követés abbahagyása",
   "account.unmute": "Unmute @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "You can press {combo} to skip this next time",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.public_timeline": "the public timeline",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
   "follow_request.authorize": "Authorize",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Show boosts",
   "home.column_settings.show_replies": "Show replies",
   "home.settings": "Column settings",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Bezárás",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Betöltés...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Blocked users",
   "navigation_bar.community_timeline": "Local timeline",
   "navigation_bar.edit_profile": "Profil szerkesztése",
   "navigation_bar.favourites": "Favourites",
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "Extended information",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Kijelentkezés",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Kezdőlap",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Média hozzáadása",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index f73ef0e19..66aff71f0 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -7,17 +7,22 @@
   "account.followers": "Pengikut",
   "account.follows": "Mengikuti",
   "account.follows_you": "Mengikuti anda",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Balasan @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Bisukan @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Postingan",
   "account.report": "Laporkan @{name}",
   "account.requested": "Menunggu persetujuan",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Hapus blokir @{name}",
   "account.unblock_domain": "Unhide {domain}",
   "account.unfollow": "Berhenti mengikuti",
   "account.unmute": "Berhenti membisukan @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Tidak ada apapun dalam hashtag ini.",
   "empty_column.home": "Anda sedang tidak mengikuti siapapun. Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.",
   "empty_column.home.public_timeline": "linimasa publik",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Anda tidak memiliki notifikasi apapun. Berinteraksi dengan orang lain untuk memulai percakapan.",
   "empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisinya secara manual",
   "follow_request.authorize": "Izinkan",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Tampilkan Boost",
   "home.column_settings.show_replies": "Tampilkan balasan",
   "home.settings": "Pengaturan kolom",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Tutup",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Tunggu sebentar...",
   "media_gallery.toggle_visible": "Tampil/Sembunyikan",
   "missing_indicator.label": "Tidak ditemukan",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Pengguna diblokir",
   "navigation_bar.community_timeline": "Linimasa lokal",
   "navigation_bar.edit_profile": "Ubah profil",
   "navigation_bar.favourites": "Favorit",
   "navigation_bar.follow_requests": "Permintaan mengikuti",
   "navigation_bar.info": "Informasi selengkapnya",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Keluar",
   "navigation_bar.mutes": "Pengguna dibisukan",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Beranda",
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Notifikasi",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Seret & lepaskan untuk mengunggah",
   "upload_button.label": "Tambahkan media",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 53371bece..673f895fe 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -7,17 +7,22 @@
   "account.followers": "Sequanti",
   "account.follows": "Sequas",
   "account.follows_you": "Sequas tu",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Mencionar @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Celar @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Mesaji",
   "account.report": "Denuncar @{name}",
   "account.requested": "Vartante aprobo",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Desblokusar @{name}",
   "account.unblock_domain": "Unhide {domain}",
   "account.unfollow": "Ne plus sequar",
   "account.unmute": "Ne plus celar @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Esas ankore nulo en ta gretovorto.",
   "empty_column.home": "Tu sequas ankore nulu. Vizitez {public} od uzez la serchilo por komencar e renkontrar altra uzeri.",
   "empty_column.home.public_timeline": "la publika tempolineo",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Tu havas ankore nula savigo. Komunikez kun altri por debutar la konverso.",
   "empty_column.public": "Esas nulo hike! Skribez ulo publike, o manuale sequez uzeri de altra instaluri por plenigar ol.",
   "follow_request.authorize": "Yurizar",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Montrar repeti",
   "home.column_settings.show_replies": "Montrar respondi",
   "home.settings": "Aranji di la kolumno",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Klozar",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Kargante...",
   "media_gallery.toggle_visible": "Chanjar videbleso",
   "missing_indicator.label": "Ne trovita",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Blokusita uzeri",
   "navigation_bar.community_timeline": "Lokala tempolineo",
   "navigation_bar.edit_profile": "Modifikar profilo",
   "navigation_bar.favourites": "Favorati",
   "navigation_bar.follow_requests": "Demandi di sequado",
   "navigation_bar.info": "Detaloza informi",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Ekirar",
   "navigation_bar.mutes": "Celita uzeri",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Hemo",
   "tabs_bar.local_timeline": "Lokala",
   "tabs_bar.notifications": "Savigi",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Tranar faligar por kargar",
   "upload_button.label": "Adjuntar kontenajo",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 3873d797e..3d2d47471 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -7,17 +7,22 @@
   "account.followers": "Seguaci",
   "account.follows": "Segue",
   "account.follows_you": "Ti segue",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Menziona @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Silenzia @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Posts",
   "account.report": "Segnala @{name}",
   "account.requested": "In attesa di approvazione",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Sblocca @{name}",
   "account.unblock_domain": "Unhide {domain}",
   "account.unfollow": "Non seguire",
   "account.unmute": "Non silenziare @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Non c'è ancora nessun post con questo hashtag.",
   "empty_column.home": "Non stai ancora seguendo nessuno. Visita {public} o usa la ricerca per incontrare nuove persone.",
   "empty_column.home.public_timeline": "la timeline pubblica",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Non hai ancora nessuna notifica. Interagisci con altri per iniziare conversazioni.",
   "empty_column.public": "Qui non c'è nulla! Scrivi qualcosa pubblicamente, o aggiungi utenti da altri server per riempire questo spazio.",
   "follow_request.authorize": "Autorizza",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Mostra post condivisi",
   "home.column_settings.show_replies": "Mostra risposte",
   "home.settings": "Impostazioni colonna",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Chiudi",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Carico...",
   "media_gallery.toggle_visible": "Imposta visibilità",
   "missing_indicator.label": "Non trovato",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Utenti bloccati",
   "navigation_bar.community_timeline": "Timeline locale",
   "navigation_bar.edit_profile": "Modifica profilo",
   "navigation_bar.favourites": "Apprezzati",
   "navigation_bar.follow_requests": "Richieste di amicizia",
   "navigation_bar.info": "Informazioni estese",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Utenti silenziati",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Home",
   "tabs_bar.local_timeline": "Locale",
   "tabs_bar.notifications": "Notifiche",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Trascina per caricare",
   "upload_button.label": "Aggiungi file multimediale",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index fb6d11ebe..388dd57e8 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -7,17 +7,22 @@
   "account.followers": "フォロワー",
   "account.follows": "フォロー",
   "account.follows_you": "フォローされています",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "メディア",
   "account.mention": "返信",
+  "account.moved_to": "{name}さんは引っ越しました:",
   "account.mute": "ミュート",
+  "account.mute_notifications": "@{name}からの通知を受け取る",
   "account.posts": "投稿",
   "account.report": "通報",
   "account.requested": "承認待ち",
   "account.share": "@{name} のプロフィールを共有する",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "ブロック解除",
   "account.unblock_domain": "{domain}を表示",
   "account.unfollow": "フォロー解除",
   "account.unmute": "ミュート解除",
+  "account.unmute_notifications": "@{name}からの通知を受け取らない",
   "account.view_full_profile": "全ての情報を見る",
   "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
   "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "このハッシュタグはまだ使われていません。",
   "empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
   "empty_column.home.public_timeline": "連合タイムライン",
+  "empty_column.list": "このリストにはまだなにもありません。",
   "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
   "empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
   "follow_request.authorize": "許可",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "ブースト表示",
   "home.column_settings.show_replies": "返信表示",
   "home.settings": "カラム設定",
+  "keyboard_shortcuts.back": "戻る",
+  "keyboard_shortcuts.boost": "ブースト",
+  "keyboard_shortcuts.column": "左からn番目のカラム内最新トゥートに移動",
+  "keyboard_shortcuts.compose": "トゥート入力欄に移動",
+  "keyboard_shortcuts.description": "説明",
+  "keyboard_shortcuts.down": "カラム内一つ下に移動",
+  "keyboard_shortcuts.enter": "トゥートの詳細を表示",
+  "keyboard_shortcuts.favourite": "お気に入り",
+  "keyboard_shortcuts.heading": "キーボードショートカット一覧",
+  "keyboard_shortcuts.hotkey": "ホットキー",
+  "keyboard_shortcuts.legend": "この一覧を表示",
+  "keyboard_shortcuts.mention": "メンション",
+  "keyboard_shortcuts.reply": "返信",
+  "keyboard_shortcuts.search": "検索欄に移動",
+  "keyboard_shortcuts.toot": "新規トゥート",
+  "keyboard_shortcuts.unfocus": "トゥート入力欄・検索欄から離れる",
+  "keyboard_shortcuts.up": "カラム内一つ上に移動",
   "lightbox.close": "閉じる",
   "lightbox.next": "次",
   "lightbox.previous": "前",
   "loading_indicator.label": "読み込み中...",
   "media_gallery.toggle_visible": "表示切り替え",
   "missing_indicator.label": "見つかりません",
+  "mute_modal.hide_notifications": "このユーザーからの通知を隠しますか?",
   "navigation_bar.blocks": "ブロックしたユーザー",
   "navigation_bar.community_timeline": "ローカルタイムライン",
   "navigation_bar.edit_profile": "プロフィールを編集",
   "navigation_bar.favourites": "お気に入り",
   "navigation_bar.follow_requests": "フォローリクエスト",
   "navigation_bar.info": "このインスタンスについて",
+  "navigation_bar.keyboard_shortcuts": "キーボードショートカット",
   "navigation_bar.logout": "ログアウト",
   "navigation_bar.mutes": "ミュートしたユーザー",
   "navigation_bar.pins": "固定されたトゥート",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "ホーム",
   "tabs_bar.local_timeline": "ローカル",
   "tabs_bar.notifications": "通知",
+  "ui.beforeunload": "Mastodonから離れるとあなたのドラフトは失われます。",
   "upload_area.title": "ドラッグ&ドロップでアップロード",
   "upload_button.label": "メディアを追加",
   "upload_form.description": "視覚障害者のための説明",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 2919928af..03028fc31 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -7,17 +7,22 @@
   "account.followers": "팔로워",
   "account.follows": "팔로우",
   "account.follows_you": "날 팔로우합니다",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "미디어",
   "account.mention": "답장",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "뮤트",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "포스트",
   "account.report": "신고",
   "account.requested": "승인 대기 중",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "차단 해제",
   "account.unblock_domain": "{domain} 숨김 해제",
   "account.unfollow": "팔로우 해제",
   "account.unmute": "뮤트 해제",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "전체 프로필 보기",
   "boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,11 +88,12 @@
   "empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.",
   "empty_column.home": "아직 아무도 팔로우 하고 있지 않습니다. {public}를 보러 가거나, 검색하여 다른 사용자를 찾아 보세요.",
   "empty_column.home.public_timeline": "연합 타임라인",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요!",
   "empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 인스턴스 유저를 팔로우 해서 가득 채워보세요!",
   "follow_request.authorize": "허가",
   "follow_request.reject": "거부",
-  "getting_started.appsshort": "어플리케이션",
+  "getting_started.appsshort": "애플리케이션",
   "getting_started.faq": "자주 있는 질문",
   "getting_started.heading": "시작",
   "getting_started.open_source_notice": "Mastodon은 오픈 소스 소프트웨어입니다. 누구나 GitHub({github})에서 개발에 참여하거나, 문제를 보고할 수 있습니다.",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "부스트 표시",
   "home.column_settings.show_replies": "답글 표시",
   "home.settings": "컬럼 설정",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "닫기",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "불러오는 중...",
   "media_gallery.toggle_visible": "표시 전환",
   "missing_indicator.label": "찾을 수 없습니다",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "차단한 사용자",
   "navigation_bar.community_timeline": "로컬 타임라인",
   "navigation_bar.edit_profile": "프로필 편집",
   "navigation_bar.favourites": "즐겨찾기",
   "navigation_bar.follow_requests": "팔로우 요청",
   "navigation_bar.info": "이 인스턴스에 대해서",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "로그아웃",
   "navigation_bar.mutes": "뮤트 중인 사용자",
   "navigation_bar.pins": "고정된 툿",
@@ -145,7 +170,7 @@
   "onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.",
   "onboarding.page_six.guidelines": "커뮤니티 가이드라인",
   "onboarding.page_six.read_guidelines": "{guidelines}을 확인하는 것을 잊지 마세요.",
-  "onboarding.page_six.various_app": "다양한 모바일 어플리케이션",
+  "onboarding.page_six.various_app": "다양한 모바일 애플리케이션",
   "onboarding.page_three.profile": "[프로필 편집] 에서 자기 소개나 이름을 변경할 수 있습니다. 또한 다른 설정도 변경할 수 있습니다.",
   "onboarding.page_three.search": "검색 바에서 {illustration} 나 {introductions} 와 같이 특정 해시태그가 달린 포스트를 보거나, 사용자를 찾을 수 있습니다.",
   "onboarding.page_two.compose": "이 폼에서 포스팅 할 수 있습니다. 이미지나 공개 범위 설정, 스포일러 경고 설정은 아래 아이콘으로 설정할 수 있습니다.",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "홈",
   "tabs_bar.local_timeline": "로컬",
   "tabs_bar.notifications": "알림",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "드래그 & 드롭으로 업로드",
   "upload_button.label": "미디어 추가",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index d0727a24d..218709899 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -7,17 +7,22 @@
   "account.followers": "Volgers",
   "account.follows": "Volgt",
   "account.follows_you": "Volgt jou",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Vermeld @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Negeer @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Toots",
   "account.report": "Rapporteer @{name}",
   "account.requested": "Wacht op goedkeuring. Klik om volgverzoek te annuleren.",
   "account.share": "Profiel van @{name} delen",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Deblokkeer @{name}",
   "account.unblock_domain": "{domain} niet meer negeren",
   "account.unfollow": "Ontvolgen",
   "account.unmute": "@{name} niet meer negeren",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "Volledig profiel tonen",
   "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
   "bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.",
   "empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.",
   "empty_column.home.public_timeline": "de globale tijdlijn",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Je hebt nog geen meldingen. Heb interactie met andere mensen om het gesprek aan te gaan.",
   "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere Mastodon-servers om het te vullen.",
   "follow_request.authorize": "Goedkeuren",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Boosts tonen",
   "home.column_settings.show_replies": "Reacties tonen",
   "home.settings": "Kolom-instellingen",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Sluiten",
   "lightbox.next": "Volgende",
   "lightbox.previous": "Vorige",
   "loading_indicator.label": "Laden…",
   "media_gallery.toggle_visible": "Media wel/niet tonen",
   "missing_indicator.label": "Niet gevonden",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Geblokkeerde gebruikers",
   "navigation_bar.community_timeline": "Lokale tijdlijn",
   "navigation_bar.edit_profile": "Profiel bewerken",
   "navigation_bar.favourites": "Favorieten",
   "navigation_bar.follow_requests": "Volgverzoeken",
   "navigation_bar.info": "Uitgebreide informatie",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Afmelden",
   "navigation_bar.mutes": "Genegeerde gebruikers",
   "navigation_bar.pins": "Vastgezette toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Start",
   "tabs_bar.local_timeline": "Lokaal",
   "tabs_bar.notifications": "Meldingen",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Hierin slepen om te uploaden",
   "upload_button.label": "Media toevoegen",
   "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index d74ae0091..234589d34 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -7,17 +7,22 @@
   "account.followers": "Følgere",
   "account.follows": "Følger",
   "account.follows_you": "Følger deg",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Nevn @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Demp @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Innlegg",
   "account.report": "Rapportér @{name}",
   "account.requested": "Venter på godkjennelse",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Avblokker @{name}",
   "account.unblock_domain": "Vis {domain}",
   "account.unfollow": "Avfølg",
   "account.unmute": "Avdemp @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
   "empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.",
   "empty_column.home.public_timeline": "en offentlig tidslinje",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
   "empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
   "follow_request.authorize": "Autorisér",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Vis fremhevinger",
   "home.column_settings.show_replies": "Vis svar",
   "home.settings": "Kolonneinnstillinger",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Lukk",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Laster...",
   "media_gallery.toggle_visible": "Veksle synlighet",
   "missing_indicator.label": "Ikke funnet",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Blokkerte brukere",
   "navigation_bar.community_timeline": "Lokal tidslinje",
   "navigation_bar.edit_profile": "Rediger profil",
   "navigation_bar.favourites": "Likt",
   "navigation_bar.follow_requests": "Følgeforespørsler",
   "navigation_bar.info": "Utvidet informasjon",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Logg ut",
   "navigation_bar.mutes": "Dempede brukere",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Hjem",
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Varslinger",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Dra og slipp for å laste opp",
   "upload_button.label": "Legg til media",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index d826423b5..6e68d6810 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -7,17 +7,22 @@
   "account.followers": "Seguidors",
   "account.follows": "Abonaments",
   "account.follows_you": "Vos sèc",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Mèdias",
   "account.mention": "Mencionar @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Rescondre @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Estatuts",
   "account.report": "Senhalar @{name}",
   "account.requested": "Invitacion mandada. Clicatz per anullar.",
   "account.share": "Partejar lo perfil a @{name}",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Desblocar @{name}",
   "account.unblock_domain": "Desblocar {domain}",
   "account.unfollow": "Quitar de sègre",
   "account.unmute": "Quitar de rescondre @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "Veire lo perfil complet",
   "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
   "bundle_column_error.body": "Quicòm a fach meuca pendent lo cargament d’aqueste compausant.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
   "empty_column.home": "Vòstre flux d’acuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
   "empty_column.home.public_timeline": "lo flux public",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
   "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.",
   "follow_request.authorize": "Autorizar",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Mostrar los partatges",
   "home.column_settings.show_replies": "Mostrar las responsas",
   "home.settings": "Paramètres de la colomna",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Tampar",
   "lightbox.next": "Seguent",
   "lightbox.previous": "Precedent",
   "loading_indicator.label": "Cargament…",
   "media_gallery.toggle_visible": "Modificar la visibilitat",
   "missing_indicator.label": "Pas trobat",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Personas blocadas",
   "navigation_bar.community_timeline": "Flux public local",
   "navigation_bar.edit_profile": "Modificar lo perfil",
   "navigation_bar.favourites": "Favorits",
   "navigation_bar.follow_requests": "Demandas d'abonament",
   "navigation_bar.info": "Mai informacions",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Desconnexion",
   "navigation_bar.mutes": "Personas rescondudas",
   "navigation_bar.pins": "Tuts penjats",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Acuèlh",
   "tabs_bar.local_timeline": "Flux public local",
   "tabs_bar.notifications": "Notificacions",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Lisatz e depausatz per mandar",
   "upload_button.label": "Ajustar un mèdia",
   "upload_form.description": "Descripcion pels mal vesents",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index b23a5e69f..907999940 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -7,17 +7,22 @@
   "account.followers": "Śledzący",
   "account.follows": "Śledzeni",
   "account.follows_you": "Śledzi Cię",
+  "account.hide_reblogs": "Ukryj podbicia od @{name}",
   "account.media": "Media",
   "account.mention": "Wspomnij o @{name}",
+  "account.moved_to": "{name} przeniósł się do:",
   "account.mute": "Wycisz @{name}",
+  "account.mute_notifications": "Wycisz powiadomienia o @{name}",
   "account.posts": "Wpisy",
   "account.report": "Zgłoś @{name}",
   "account.requested": "Oczekująca prośba, kliknij aby anulować",
   "account.share": "Udostępnij profil @{name}",
+  "account.show_reblogs": "Pokazuj podbicia od @{name}",
   "account.unblock": "Odblokuj @{name}",
   "account.unblock_domain": "Odblokuj domenę {domain}",
   "account.unfollow": "Przestań śledzić",
   "account.unmute": "Cofnij wyciszenie @{name}",
+  "account.unmute_notifications": "Cofnij wyciszenie powiadomień od @{name}",
   "account.view_full_profile": "Wyświetl pełny profil",
   "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
   "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Nie ma wpisów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
   "empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
   "empty_column.home.public_timeline": "publiczna oś czasu",
+  "empty_column.list": "Nie ma nic na tej liście.",
   "empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.",
   "empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych instancji, aby to wyświetlić.",
   "follow_request.authorize": "Autoryzuj",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Pokazuj podbicia",
   "home.column_settings.show_replies": "Pokazuj odpowiedzi",
   "home.settings": "Ustawienia kolumny",
+  "keyboard_shortcuts.back": "aby cofnąć się",
+  "keyboard_shortcuts.boost": "aby podbić wpis",
+  "keyboard_shortcuts.column": "aby przejść do wpisu z jednej z kolumn",
+  "keyboard_shortcuts.compose": "aby przejść do pola tworzenia wpisu",
+  "keyboard_shortcuts.description": "Opis",
+  "keyboard_shortcuts.down": "aby przejść na dół listy",
+  "keyboard_shortcuts.enter": "aby otworzyć wpis",
+  "keyboard_shortcuts.favourite": "aby dodać do ulubionych",
+  "keyboard_shortcuts.heading": "Skróty klawiszowe",
+  "keyboard_shortcuts.hotkey": "Klawisz",
+  "keyboard_shortcuts.legend": "aby wyświetlić tą legendę",
+  "keyboard_shortcuts.mention": "aby wspomnieć o autorze",
+  "keyboard_shortcuts.reply": "aby odpowiedzieć",
+  "keyboard_shortcuts.search": "aby przejść do pola wyszukiwania",
+  "keyboard_shortcuts.toot": "aby utworzyć nowy wpis",
+  "keyboard_shortcuts.unfocus": "aby opuścić pole wyszukiwania/pisania",
+  "keyboard_shortcuts.up": "aby przejść na górę listy",
   "lightbox.close": "Zamknij",
   "lightbox.next": "Następne",
   "lightbox.previous": "Poprzednie",
   "loading_indicator.label": "Ładowanie…",
   "media_gallery.toggle_visible": "Przełącz widoczność",
   "missing_indicator.label": "Nie znaleziono",
+  "mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?",
   "navigation_bar.blocks": "Zablokowani użytkownicy",
   "navigation_bar.community_timeline": "Lokalna oś czasu",
   "navigation_bar.edit_profile": "Edytuj profil",
   "navigation_bar.favourites": "Ulubione",
   "navigation_bar.follow_requests": "Prośby o śledzenie",
   "navigation_bar.info": "Szczegółowe informacje",
+  "navigation_bar.keyboard_shortcuts": "Skróty klawiszowe",
   "navigation_bar.logout": "Wyloguj",
   "navigation_bar.mutes": "Wyciszeni użytkownicy",
   "navigation_bar.pins": "Przypięte wpisy",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Strona główna",
   "tabs_bar.local_timeline": "Lokalne",
   "tabs_bar.notifications": "Powiadomienia",
+  "ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.",
   "upload_area.title": "Przeciągnij i upuść aby wysłać",
   "upload_button.label": "Dodaj zawartość multimedialną",
   "upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index a04d1cc31..ba0d2093e 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -7,17 +7,22 @@
   "account.followers": "Seguidores",
   "account.follows": "Segue",
   "account.follows_you": "Segue você",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Mídia",
   "account.mention": "Mencionar @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Silenciar @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Posts",
   "account.report": "Denunciar @{name}",
   "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação.",
   "account.share": "Compartilhar perfil de @{name}",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Desbloquear @{name}",
   "account.unblock_domain": "Desbloquear {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "Ver perfil completo",
   "boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez",
   "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag",
   "empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.",
   "empty_column.home.public_timeline": "global",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar!",
   "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias.",
   "follow_request.authorize": "Autorizar",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Mostrar compartilhamentos",
   "home.column_settings.show_replies": "Mostrar as respostas",
   "home.settings": "Configurações de colunas",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Fechar",
   "lightbox.next": "Próximo",
   "lightbox.previous": "Anterior",
   "loading_indicator.label": "Carregando...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Usuários bloqueados",
   "navigation_bar.community_timeline": "Local",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritos",
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Usuários silenciados",
   "navigation_bar.pins": "Postagens fixadas",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Página inicial",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar mídia",
   "upload_form.description": "Descreva a imagem para deficientes visuais",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 9ae140df9..f349bd191 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -7,17 +7,22 @@
   "account.followers": "Seguidores",
   "account.follows": "Segue",
   "account.follows_you": "É teu seguidor",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Mencionar @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Silenciar @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Posts",
   "account.report": "Denunciar @{name}",
   "account.requested": "A aguardar aprovação",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Não bloquear @{name}",
   "account.unblock_domain": "Unhide {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag",
   "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
   "empty_column.home.public_timeline": "global",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
   "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
   "follow_request.authorize": "Autorizar",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Mostrar as partilhas",
   "home.column_settings.show_replies": "Mostrar as respostas",
   "home.settings": "Parâmetros da listagem",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Fechar",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Carregando...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Utilizadores bloqueados",
   "navigation_bar.community_timeline": "Local",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritos",
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Utilizadores silenciados",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Home",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar media",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 65bc5b374..df52eca14 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -7,17 +7,22 @@
   "account.followers": "Подписаны",
   "account.follows": "Подписки",
   "account.follows_you": "Подписан(а) на Вас",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Медиаконтент",
   "account.mention": "Упомянуть",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Заглушить",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Посты",
   "account.report": "Пожаловаться",
   "account.requested": "Ожидает подтверждения",
   "account.share": "Поделиться профилем @{name}",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Разблокировать",
   "account.unblock_domain": "Разблокировать {domain}",
   "account.unfollow": "Отписаться",
   "account.unmute": "Снять глушение",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "Показать полный профиль",
   "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
   "bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Статусов с таким хэштегом еще не существует.",
   "empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.",
   "empty_column.home.public_timeline": "публичные ленты",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.",
   "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.",
   "follow_request.authorize": "Авторизовать",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Показывать продвижения",
   "home.column_settings.show_replies": "Показывать ответы",
   "home.settings": "Настройки колонки",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Закрыть",
   "lightbox.next": "Далее",
   "lightbox.previous": "Назад",
   "loading_indicator.label": "Загрузка...",
   "media_gallery.toggle_visible": "Показать/скрыть",
   "missing_indicator.label": "Не найдено",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Список блокировки",
   "navigation_bar.community_timeline": "Локальная лента",
   "navigation_bar.edit_profile": "Изменить профиль",
   "navigation_bar.favourites": "Понравившееся",
   "navigation_bar.follow_requests": "Запросы на подписку",
   "navigation_bar.info": "Об узле",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Выйти",
   "navigation_bar.mutes": "Список глушения",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Главная",
   "tabs_bar.local_timeline": "Локальная",
   "tabs_bar.notifications": "Уведомления",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Перетащите сюда, чтобы загрузить",
   "upload_button.label": "Добавить медиаконтент",
   "upload_form.description": "Описать для людей с нарушениями зрения",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 70beb70f7..5f887ef34 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -7,17 +7,22 @@
   "account.followers": "Följare",
   "account.follows": "Följer",
   "account.follows_you": "Följer dig",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Nämna @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Tysta @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Inlägg",
   "account.report": "Rapportera @{name}",
   "account.requested": "Inväntar godkännande. Klicka för att avbryta följförfrågan",
   "account.share": "Dela @{name}'s profil",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Avblockera @{name}",
   "account.unblock_domain": "Ta fram {domain}",
   "account.unfollow": "Sluta följa",
   "account.unmute": "Ta bort tystad @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "Visa hela profilen",
   "boost_modal.combo": "Du kan trycka {combo} för att slippa denna nästa gång",
   "bundle_column_error.body": "Något gick fel när du laddade denna komponent.",
@@ -82,8 +87,8 @@
   "empty_column.community": "Den lokala tidslinjen är tom. Skriv något offentligt för att få bollen att rulla!",
   "empty_column.hashtag": "Det finns inget i denna hashtag ännu.",
   "empty_column.home": "Din hemma-tidslinje är tom! Besök {public} eller använd sökning för att komma igång och träffa andra användare.",
-  "empty_column.home.inactivity": "Ditt hemmafeed är tomt. Om du har varit inaktiv ett tag kommer det att regenereras för dig snart.",
   "empty_column.home.public_timeline": "den publika tidslinjen",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Du har inga meddelanden än. Interagera med andra för att starta konversationen.",
   "empty_column.public": "Det finns inget här! Skriv något offentligt, eller följ manuellt användarna från andra instanser för att fylla på det",
   "follow_request.authorize": "Godkänn",
@@ -99,22 +104,40 @@
   "home.column_settings.show_reblogs": "Visa knuffar",
   "home.column_settings.show_replies": "Visa svar",
   "home.settings": "Kolumninställningar",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Stäng",
   "lightbox.next": "Nästa",
   "lightbox.previous": "Tidigare",
   "loading_indicator.label": "Laddar...",
   "media_gallery.toggle_visible": "Växla synlighet",
   "missing_indicator.label": "Hittades inte",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Blockerade användare",
   "navigation_bar.community_timeline": "Lokal tidslinje",
   "navigation_bar.edit_profile": "Redigera profil",
   "navigation_bar.favourites": "Favoriter",
   "navigation_bar.follow_requests": "Följförfrågningar",
   "navigation_bar.info": "Om denna instans",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Logga ut",
   "navigation_bar.mutes": "Tystade användare",
   "navigation_bar.pins": "Nålade inlägg (toots)",
-
   "navigation_bar.preferences": "Inställningar",
   "navigation_bar.public_timeline": "Förenad tidslinje",
   "notification.favourite": "{name} favoriserade din status",
@@ -161,6 +184,11 @@
   "privacy.public.short": "Publik",
   "privacy.unlisted.long": "Skicka inte till publik tidslinje",
   "privacy.unlisted.short": "Olistad",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Ångra",
   "report.placeholder": "Ytterligare kommentarer",
   "report.submit": "Skicka",
@@ -180,6 +208,7 @@
   "status.load_more": "Ladda fler",
   "status.media_hidden": "Media dold",
   "status.mention": "Omnämn @{name}",
+  "status.more": "More",
   "status.mute_conversation": "Tysta konversation",
   "status.open": "Utvidga denna status",
   "status.pin": "Fäst i profil",
@@ -200,6 +229,7 @@
   "tabs_bar.home": "Hem",
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Meddelanden",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Dra & släpp för att ladda upp",
   "upload_button.label": "Lägg till media",
   "upload_form.description": "Beskriv för synskadade",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index db3f3dd0d..7b2b13202 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -7,17 +7,22 @@
   "account.followers": "Followers",
   "account.follows": "Follows",
   "account.follows_you": "Follows you",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Mention @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Mute @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Posts",
   "account.report": "Report @{name}",
   "account.requested": "Awaiting approval",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Unblock @{name}",
   "account.unblock_domain": "Unhide {domain}",
   "account.unfollow": "Unfollow",
   "account.unmute": "Unmute @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "You can press {combo} to skip this next time",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.public_timeline": "the public timeline",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
   "follow_request.authorize": "Authorize",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Show boosts",
   "home.column_settings.show_replies": "Show replies",
   "home.settings": "Column settings",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Close",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Loading...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Blocked users",
   "navigation_bar.community_timeline": "Local timeline",
   "navigation_bar.edit_profile": "Edit profile",
   "navigation_bar.favourites": "Favourites",
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "About this instance",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Home",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index cdd3581da..dd5a96bdb 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -7,17 +7,22 @@
   "account.followers": "Takipçiler",
   "account.follows": "Takip ettikleri",
   "account.follows_you": "Seni takip ediyor",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Bahset @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Sustur @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Gönderiler",
   "account.report": "Rapor et @{name}",
   "account.requested": "Onay bekleniyor",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Engeli kaldır @{name}",
   "account.unblock_domain": "Unhide {domain}",
   "account.unfollow": "Takipten vazgeç",
   "account.unmute": "Sesi aç @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "Bir dahaki sefere {combo} tuşuna basabilirsiniz",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Henüz bu hashtag’e sahip hiçbir gönderi yok.",
   "empty_column.home": "Henüz kimseyi takip etmiyorsunuz. {public} ziyaret edebilir veya arama kısmını kullanarak diğer kullanıcılarla iletişime geçebilirsiniz.",
   "empty_column.home.public_timeline": "herkese açık zaman tüneli",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "Henüz hiçbir bildiriminiz yok. Diğer insanlarla sobhet edebilmek için etkileşime geçebilirsiniz.",
   "empty_column.public": "Burada hiçbir gönderi yok! Herkese açık bir şeyler yazın, veya diğer sunucudaki insanları takip ederek bu alanın dolmasını sağlayın",
   "follow_request.authorize": "Yetkilendir",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Boost edilenleri göster",
   "home.column_settings.show_replies": "Cevapları göster",
   "home.settings": "Kolon ayarları",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Kapat",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Yükleniyor...",
   "media_gallery.toggle_visible": "Görünürlüğü değiştir",
   "missing_indicator.label": "Bulunamadı",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Engellenen kullanıcılar",
   "navigation_bar.community_timeline": "Yerel zaman tüneli",
   "navigation_bar.edit_profile": "Profili düzenle",
   "navigation_bar.favourites": "Favoriler",
   "navigation_bar.follow_requests": "Takip istekleri",
   "navigation_bar.info": "Genişletilmiş bilgi",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Çıkış",
   "navigation_bar.mutes": "Sessize alınmış kullanıcılar",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Ana sayfa",
   "tabs_bar.local_timeline": "Yerel",
   "tabs_bar.notifications": "Bildirimler",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Upload için sürükle bırak yapınız",
   "upload_button.label": "Görsel ekle",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 930529f15..6206cd65b 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -7,17 +7,22 @@
   "account.followers": "Підписники",
   "account.follows": "Підписки",
   "account.follows_you": "Підписаний(-а) на Вас",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Медія",
   "account.mention": "Згадати",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "Заглушити",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Пости",
   "account.report": "Поскаржитися",
   "account.requested": "Очікує підтвердження",
   "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Розблокувати",
   "account.unblock_domain": "Розблокувати {domain}",
   "account.unfollow": "Відписатися",
   "account.unmute": "Зняти глушення",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "View full profile",
   "boost_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу",
   "bundle_column_error.body": "Something went wrong while loading this component.",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "Дописів з цим хештегом поки не існує.",
   "empty_column.home": "Ви поки ні на кого не підписані. Погортайте {public}, або скористуйтесь пошуком, щоб освоїтися та познайомитися з іншими користувачами.",
   "empty_column.home.public_timeline": "публічні стрічки",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "У вас ще немає сповіщень. Переписуйтесь з іншими користувачами, щоб почати розмову.",
   "empty_column.public": "Тут поки нічого немає! Опублікуйте щось, або вручну підпишіться на користувачів інших інстанцій, щоб заповнити стрічку.",
   "follow_request.authorize": "Авторизувати",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "Показувати передмухи",
   "home.column_settings.show_replies": "Показувати відповіді",
   "home.settings": "Налаштування колонок",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Закрити",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "loading_indicator.label": "Завантаження...",
   "media_gallery.toggle_visible": "Показати/приховати",
   "missing_indicator.label": "Не знайдено",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Заблоковані користувачі",
   "navigation_bar.community_timeline": "Локальна стрічка",
   "navigation_bar.edit_profile": "Редагувати профіль",
   "navigation_bar.favourites": "Вподобане",
   "navigation_bar.follow_requests": "Запити на підписку",
   "navigation_bar.info": "Про інстанцію",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "Вийти",
   "navigation_bar.mutes": "Заглушені користувачі",
   "navigation_bar.pins": "Pinned toots",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "Головна",
   "tabs_bar.local_timeline": "Локальна",
   "tabs_bar.notifications": "Сповіщення",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Перетягніть сюди, щоб завантажити",
   "upload_button.label": "Додати медіаконтент",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/whitelist_sv.json b/app/javascript/mastodon/locales/whitelist_sv.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_sv.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 0f4bbb7c1..c23868cda 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -1,23 +1,28 @@
 {
   "account.block": "屏蔽 @{name}",
-  "account.block_domain": "隐藏一切来自 {domain} 的嘟文",
+  "account.block_domain": "隐藏来自 {domain} 的内容",
   "account.disclaimer_full": "此处显示的信息可能不是全部内容。",
   "account.edit_profile": "修改个人资料",
   "account.follow": "关注",
   "account.followers": "关注者",
   "account.follows": "正在关注",
   "account.follows_you": "关注了你",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "媒体",
   "account.mention": "提及 @{name}",
-  "account.mute": "静音 @{name}",
+  "account.moved_to": "{name} 已经迁移到:",
+  "account.mute": "隐藏 @{name}",
+  "account.mute_notifications": "隐藏来自 @{name} 的通知",
   "account.posts": "嘟文",
   "account.report": "举报 @{name}",
   "account.requested": "正在等待对方同意。点击以取消发送关注请求",
   "account.share": "分享 @{name} 的个人资料",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "不再屏蔽 @{name}",
-  "account.unblock_domain": "不再隐藏 {domain}",
+  "account.unblock_domain": "不再隐藏来自 {domain} 的内容",
   "account.unfollow": "取消关注",
-  "account.unmute": "不再静音 @{name}",
+  "account.unmute": "不再隐藏 @{name}",
+  "account.unmute_notifications": "不再隐藏来自 @{name} 的通知",
   "account.view_full_profile": "查看完整资料",
   "boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
   "bundle_column_error.body": "载入组件出错。",
@@ -31,7 +36,7 @@
   "column.favourites": "收藏过的嘟文",
   "column.follow_requests": "关注请求",
   "column.home": "主页",
-  "column.mutes": "被静音的用户",
+  "column.mutes": "被隐藏的用户",
   "column.notifications": "通知",
   "column.pins": "置顶嘟文",
   "column.public": "跨站公共时间轴",
@@ -57,10 +62,10 @@
   "confirmations.block.message": "想好了,真的要屏蔽 {name}?",
   "confirmations.delete.confirm": "删除",
   "confirmations.delete.message": "想好了,真的要删除这条嘟文?",
-  "confirmations.domain_block.confirm": "隐藏整个网站",
-  "confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain}?多数情况下,屏蔽或静音几个特定的用户就应该能满足你的需要了。",
-  "confirmations.mute.confirm": "静音",
-  "confirmations.mute.message": "想好了,真的要静音 {name}?",
+  "confirmations.domain_block.confirm": "隐藏整个网站的内容",
+  "confirmations.domain_block.message": "你真的真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户就应该能满足你的需要了。",
+  "confirmations.mute.confirm": "隐藏",
+  "confirmations.mute.message": "想好了,真的要隐藏 {name}?",
   "confirmations.unfollow.confirm": "取消关注",
   "confirmations.unfollow.message": "确定要取消关注 {name} 吗?",
   "embed.instructions": "要在你的网站上嵌入这条嘟文,请复制以下代码。",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "这个话题标签下暂时没有内容。",
   "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
   "empty_column.home.public_timeline": "公共时间轴",
+  "empty_column.list": "这个列表中暂时没有内容。",
   "empty_column.notifications": "你还没有收到过通知信息,快向其他用户搭讪吧。",
   "empty_column.public": "这里神马都没有!写一些公开的嘟文,或者关注其他实例的用户,这里就会有嘟文出现了哦!",
   "follow_request.authorize": "同意",
@@ -98,20 +104,39 @@
   "home.column_settings.show_reblogs": "显示转嘟",
   "home.column_settings.show_replies": "显示回复",
   "home.settings": "栏目设置",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "关闭",
   "lightbox.next": "下一步",
   "lightbox.previous": "上一步",
   "loading_indicator.label": "加载中……",
   "media_gallery.toggle_visible": "切换显示/隐藏",
   "missing_indicator.label": "找不到内容",
+  "mute_modal.hide_notifications": "隐藏来自这个用户的通知",
   "navigation_bar.blocks": "被屏蔽的用户",
   "navigation_bar.community_timeline": "本站时间轴",
   "navigation_bar.edit_profile": "修改个人资料",
   "navigation_bar.favourites": "收藏的内容",
   "navigation_bar.follow_requests": "关注请求",
   "navigation_bar.info": "关于本站",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "注销",
-  "navigation_bar.mutes": "被静音的用户",
+  "navigation_bar.mutes": "被隐藏的用户",
   "navigation_bar.pins": "置顶嘟文",
   "navigation_bar.preferences": "首选项",
   "navigation_bar.public_timeline": "跨站公共时间轴",
@@ -184,7 +209,7 @@
   "status.media_hidden": "隐藏媒体内容",
   "status.mention": "提及 @{name}",
   "status.more": "更多",
-  "status.mute_conversation": "静音此对话",
+  "status.mute_conversation": "隐藏此对话",
   "status.open": "展开嘟文",
   "status.pin": "在个人资料页面置顶",
   "status.reblog": "转嘟",
@@ -197,7 +222,7 @@
   "status.share": "分享",
   "status.show_less": "隐藏内容",
   "status.show_more": "显示内容",
-  "status.unmute_conversation": "不再静音此对话",
+  "status.unmute_conversation": "不再隐藏此对话",
   "status.unpin": "在个人资料页面取消置顶",
   "tabs_bar.compose": "撰写",
   "tabs_bar.federated_timeline": "跨站",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index f6f56fee1..a1c7aa875 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -7,17 +7,22 @@
   "account.followers": "關注的人",
   "account.follows": "正關注",
   "account.follows_you": "關注你",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "媒體",
   "account.mention": "提及 @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "將 @{name} 靜音",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "文章",
   "account.report": "舉報 @{name}",
   "account.requested": "等候審批",
   "account.share": "分享 @{name} 的個人資料",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "解除對 @{name} 的封鎖",
   "account.unblock_domain": "不再隱藏 {domain}",
   "account.unfollow": "取消關注",
   "account.unmute": "取消 @{name} 的靜音",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "查看完整資料",
   "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
   "bundle_column_error.body": "加載本組件出錯。",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "這個標籤暫時未有內容。",
   "empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
   "empty_column.home.public_timeline": "公共時間軸",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。",
   "empty_column.public": "跨站時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。",
   "follow_request.authorize": "批准",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "顯示被轉推的文章",
   "home.column_settings.show_replies": "顯示回應文章",
   "home.settings": "欄位設定",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "關閉",
   "lightbox.next": "繼續",
   "lightbox.previous": "回退",
   "loading_indicator.label": "載入中...",
   "media_gallery.toggle_visible": "打開或關上",
   "missing_indicator.label": "找不到內容",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "被你封鎖的用戶",
   "navigation_bar.community_timeline": "本站時間軸",
   "navigation_bar.edit_profile": "修改個人資料",
   "navigation_bar.favourites": "最愛的內容",
   "navigation_bar.follow_requests": "關注請求",
   "navigation_bar.info": "關於本服務站",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "登出",
   "navigation_bar.mutes": "被你靜音的用戶",
   "navigation_bar.pins": "置頂文章",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "主頁",
   "tabs_bar.local_timeline": "本站",
   "tabs_bar.notifications": "通知",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "將檔案拖放至此上載",
   "upload_button.label": "上載媒體檔案",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 1f43c6a20..b52c5a810 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -7,17 +7,22 @@
   "account.followers": "專注者",
   "account.follows": "正關注",
   "account.follows_you": "關注你",
+  "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "媒體",
   "account.mention": "提到 @{name}",
+  "account.moved_to": "{name} has moved to:",
   "account.mute": "消音 @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "貼文",
   "account.report": "檢舉 @{name}",
   "account.requested": "正在等待許可",
   "account.share": "分享 @{name} 的用者資訊",
+  "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "取消封鎖 @{name}",
   "account.unblock_domain": "不再隱藏 {domain}",
   "account.unfollow": "取消關注",
   "account.unmute": "不再消音 @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
   "account.view_full_profile": "查看完整資訊",
   "boost_modal.combo": "下次你可以按 {combo} 來跳過",
   "bundle_column_error.body": "加載本組件出錯。",
@@ -83,6 +88,7 @@
   "empty_column.hashtag": "這個主題標籤下什麼都沒有。",
   "empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用者。",
   "empty_column.home.public_timeline": "公開時間軸",
+  "empty_column.list": "There is nothing in this list yet.",
   "empty_column.notifications": "還沒有任何通知。和別的使用者互動來開始對話。",
   "empty_column.public": "這裡什麼都沒有!公開寫些什麼,或是關注其他副本的使用者。",
   "follow_request.authorize": "授權",
@@ -98,18 +104,37 @@
   "home.column_settings.show_reblogs": "顯示轉推",
   "home.column_settings.show_replies": "顯示回應",
   "home.settings": "欄位設定",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "關閉",
   "lightbox.next": "繼續",
   "lightbox.previous": "回退",
   "loading_indicator.label": "讀取中...",
   "media_gallery.toggle_visible": "切換可見性",
   "missing_indicator.label": "找不到",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "封鎖的使用者",
   "navigation_bar.community_timeline": "本地時間軸",
   "navigation_bar.edit_profile": "編輯用者資訊",
   "navigation_bar.favourites": "最愛",
   "navigation_bar.follow_requests": "關注請求",
   "navigation_bar.info": "關於本站",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
   "navigation_bar.logout": "登出",
   "navigation_bar.mutes": "消音的使用者",
   "navigation_bar.pins": "置頂貼文",
@@ -204,6 +229,7 @@
   "tabs_bar.home": "家",
   "tabs_bar.local_timeline": "本地",
   "tabs_bar.notifications": "通知",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "拖放來上傳",
   "upload_button.label": "增加媒體",
   "upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js
index 8a4d69f26..5891d0fdd 100644
--- a/app/javascript/mastodon/reducers/accounts.js
+++ b/app/javascript/mastodon/reducers/accounts.js
@@ -59,6 +59,11 @@ const normalizeAccount = (state, account) => {
   account.display_name_html = emojify(escapeTextContentForBrowser(displayName));
   account.note_emojified = emojify(account.note);
 
+  if (account.moved) {
+    state = normalizeAccount(state, account.moved);
+    account.moved = account.moved.id;
+  }
+
   return state.set(account.id, fromJS(account));
 };
 
diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js
index 1ed0fe3e3..2a78a9f34 100644
--- a/app/javascript/mastodon/reducers/accounts_counters.js
+++ b/app/javascript/mastodon/reducers/accounts_counters.js
@@ -126,7 +126,8 @@ export default function accountsCounters(state = initialState, action) {
   case STATUS_FETCH_SUCCESS:
     return normalizeAccountFromStatus(state, action.status);
   case ACCOUNT_FOLLOW_SUCCESS:
-    return state.updateIn([action.relationship.id, 'followers_count'], num => num + 1);
+    return action.alreadyFollowing ? state :
+      state.updateIn([action.relationship.id, 'followers_count'], num => num + 1);
   case ACCOUNT_UNFOLLOW_SUCCESS:
     return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1));
   default:
diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js
index 64d584a01..fe8308d0c 100644
--- a/app/javascript/mastodon/reducers/contexts.js
+++ b/app/javascript/mastodon/reducers/contexts.js
@@ -1,3 +1,7 @@
+import {
+  ACCOUNT_BLOCK_SUCCESS,
+  ACCOUNT_MUTE_SUCCESS,
+} from '../actions/accounts';
 import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
 import { TIMELINE_DELETE, TIMELINE_CONTEXT_UPDATE } from '../actions/timelines';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
@@ -31,6 +35,12 @@ const deleteFromContexts = (state, id) => {
   return state;
 };
 
+const filterContexts = (state, relationship) => {
+  return state.map(
+    statuses => statuses.filter(
+      status => status.get('account') !== relationship.id));
+};
+
 const updateContext = (state, status, references) => {
   return state.update('descendants', map => {
     references.forEach(parentId => {
@@ -49,6 +59,9 @@ const updateContext = (state, status, references) => {
 
 export default function contexts(state = initialState, action) {
   switch(action.type) {
+  case ACCOUNT_BLOCK_SUCCESS:
+  case ACCOUNT_MUTE_SUCCESS:
+    return filterContexts(state, action.relationship);
   case CONTEXT_FETCH_SUCCESS:
     return normalizeContext(state, action.id, action.ancestors, action.descendants);
   case TIMELINE_DELETE:
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index 17c870351..425a2acdd 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -22,6 +22,7 @@ import media_attachments from './media_attachments';
 import notifications from './notifications';
 import height_cache from './height_cache';
 import custom_emojis from './custom_emojis';
+import lists from './lists';
 
 const reducers = {
   timelines,
@@ -47,6 +48,7 @@ const reducers = {
   notifications,
   height_cache,
   custom_emojis,
+  lists,
 };
 
 export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/reducers/lists.js b/app/javascript/mastodon/reducers/lists.js
new file mode 100644
index 000000000..3e3908869
--- /dev/null
+++ b/app/javascript/mastodon/reducers/lists.js
@@ -0,0 +1,15 @@
+import { LIST_FETCH_SUCCESS } from '../actions/lists';
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+const initialState = ImmutableMap();
+
+const normalizeList = (state, list) => state.set(list.id, fromJS(list));
+
+export default function lists(state = initialState, action) {
+  switch(action.type) {
+  case LIST_FETCH_SUCCESS:
+    return normalizeList(state, action.list);
+  default:
+    return state;
+  }
+};
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index b1fb4c5da..5120b2b67 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -23,10 +23,6 @@ import {
   TIMELINE_EXPAND_SUCCESS,
 } from '../actions/timelines';
 import {
-  ACCOUNT_BLOCK_SUCCESS,
-  ACCOUNT_MUTE_SUCCESS,
-} from '../actions/accounts';
-import {
   NOTIFICATIONS_UPDATE,
   NOTIFICATIONS_REFRESH_SUCCESS,
   NOTIFICATIONS_EXPAND_SUCCESS,
@@ -88,18 +84,6 @@ const deleteStatus = (state, id, references) => {
   return state.delete(id);
 };
 
-const filterStatuses = (state, relationship) => {
-  state.forEach(status => {
-    if (status.get('account') !== relationship.id) {
-      return;
-    }
-
-    state = deleteStatus(state, status.get('id'), state.filter(item => item.get('reblog') === status.get('id')));
-  });
-
-  return state;
-};
-
 const initialState = ImmutableMap();
 
 export default function statuses(state = initialState, action) {
@@ -139,9 +123,6 @@ export default function statuses(state = initialState, action) {
     return normalizeStatuses(state, action.statuses);
   case TIMELINE_DELETE:
     return deleteStatus(state, action.id, action.references);
-  case ACCOUNT_BLOCK_SUCCESS:
-  case ACCOUNT_MUTE_SUCCESS:
-    return filterStatuses(state, action.relationship);
   default:
     return state;
   }
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index bee4c4ef9..9984c3b5d 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -55,7 +55,7 @@ const appendNormalizedTimeline = (state, timeline, statuses, next) => {
   }));
 };
 
-const updateTimeline = (state, timeline, status, references) => {
+const updateTimeline = (state, timeline, status) => {
   const top        = state.getIn([timeline, 'top']);
   const ids        = state.getIn([timeline, 'items'], ImmutableList());
   const includesId = ids.includes(status.get('id'));
@@ -70,7 +70,6 @@ const updateTimeline = (state, timeline, status, references) => {
   return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
     if (!top) mMap.set('unread', unread + 1);
     if (top && ids.size > 40) newIds = newIds.take(20);
-    if (status.getIn(['reblog', 'id'], null) !== null) newIds = newIds.filterNot(item => references.includes(item));
     mMap.set('items', newIds.unshift(status.get('id')));
   }));
 };
@@ -129,7 +128,7 @@ export default function timelines(state = initialState, action) {
   case TIMELINE_EXPAND_SUCCESS:
     return appendNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next);
   case TIMELINE_UPDATE:
-    return updateTimeline(state, action.timeline, fromJS(action.status), action.references);
+    return updateTimeline(state, action.timeline, fromJS(action.status));
   case TIMELINE_DELETE:
     return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
   case ACCOUNT_BLOCK_SUCCESS:
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index d26d1b727..e47ec5183 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -4,14 +4,18 @@ import { List as ImmutableList } from 'immutable';
 const getAccountBase         = (state, id) => state.getIn(['accounts', id], null);
 const getAccountCounters     = (state, id) => state.getIn(['accounts_counters', id], null);
 const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null);
+const getAccountMoved        = (state, id) => state.getIn(['accounts', state.getIn(['accounts', id, 'moved'])]);
 
 export const makeGetAccount = () => {
-  return createSelector([getAccountBase, getAccountCounters, getAccountRelationship], (base, counters, relationship) => {
+  return createSelector([getAccountBase, getAccountCounters, getAccountRelationship, getAccountMoved], (base, counters, relationship, moved) => {
     if (base === null) {
       return null;
     }
 
-    return base.merge(counters).set('relationship', relationship);
+    return base.merge(counters).withMutations(map => {
+      map.set('relationship', relationship);
+      map.set('moved', moved);
+    });
   });
 };
 
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 87bc710af..7f078470e 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -347,3 +347,120 @@
     }
   }
 }
+
+.spacer {
+  flex: 1 1 auto;
+}
+
+.log-entry {
+  margin-bottom: 8px;
+  line-height: 20px;
+
+  &__header {
+    display: flex;
+    justify-content: flex-start;
+    align-items: center;
+    padding: 10px;
+    background: $ui-base-color;
+    color: $ui-primary-color;
+    border-radius: 4px 4px 0 0;
+    font-size: 14px;
+    position: relative;
+  }
+
+  &__avatar {
+    margin-right: 10px;
+
+    .avatar {
+      display: block;
+      margin: 0;
+      border-radius: 50%;
+      width: 40px;
+      height: 40px;
+    }
+  }
+
+  &__title {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  &__timestamp {
+    color: lighten($ui-base-color, 34%);
+  }
+
+  &__extras {
+    background: lighten($ui-base-color, 6%);
+    border-radius: 0 0 4px 4px;
+    padding: 10px;
+    color: $ui-primary-color;
+    font-family: 'mastodon-font-monospace', monospace;
+    font-size: 12px;
+    white-space: nowrap;
+    min-height: 20px;
+  }
+
+  &__icon {
+    font-size: 28px;
+    margin-right: 10px;
+    color: lighten($ui-base-color, 34%);
+  }
+
+  &__icon__overlay {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    width: 10px;
+    height: 10px;
+    border-radius: 50%;
+
+    &.positive {
+      background: $success-green;
+    }
+
+    &.negative {
+      background: $error-red;
+    }
+
+    &.neutral {
+      background: $ui-highlight-color;
+    }
+  }
+
+  a,
+  .username,
+  .target {
+    color: $ui-secondary-color;
+    text-decoration: none;
+    font-weight: 500;
+  }
+
+  .diff-old {
+    color: $error-red;
+  }
+
+  .diff-neutral {
+    color: $ui-secondary-color;
+  }
+
+  .diff-new {
+    color: $success-green;
+  }
+}
+
+.name-tag {
+  display: flex;
+  align-items: center;
+
+  .avatar {
+    display: block;
+    margin: 0;
+    margin-right: 5px;
+    border-radius: 50%;
+  }
+
+  .username {
+    font-weight: 500;
+  }
+}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 8566585c5..942a3a3fa 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1,5 +1,3 @@
-@import 'variables';
-
 .app-body {
   -webkit-overflow-scrolling: touch;
   -ms-overflow-style: -ms-autohiding-scrollbar;
@@ -917,6 +915,18 @@
   background-position: center;
   position: relative;
 
+  &.inactive {
+    opacity: 0.5;
+
+    .account__header__avatar {
+      filter: grayscale(100%);
+    }
+
+    .account__header__username {
+      color: $ui-primary-color;
+    }
+  }
+
   & > div {
     background: rgba(lighten($ui-base-color, 4%), 0.9);
     padding: 20px 10px;
@@ -2087,6 +2097,27 @@
   }
 }
 
+.keyboard-shortcuts {
+  padding: 8px 0 0;
+  overflow: hidden;
+
+  thead {
+    position: absolute;
+    left: -9999px;
+  }
+
+  td {
+    padding: 0 10px 8px;
+  }
+
+  code {
+    display: inline-block;
+    padding: 3px 5px;
+    background-color: lighten($ui-base-color, 8%);
+    border: 1px solid darken($ui-base-color, 4%);
+  }
+}
+
 .setting-text {
   color: $ui-primary-color;
   background: transparent;
@@ -2170,15 +2201,12 @@ button.icon-button.active i.fa-retweet {
 }
 
 .status-card-photo {
+  cursor: zoom-in;
   display: block;
   text-decoration: none;
-
-  img {
-    display: block;
-    width: 100%;
-    height: auto;
-    margin: 0;
-  }
+  width: 100%;
+  height: auto;
+  margin: 0;
 }
 
 .status-card-video {
@@ -4375,3 +4403,40 @@ noscript {
     }
   }
 }
+
+.account__moved-note {
+  padding: 14px 10px;
+  padding-bottom: 16px;
+  background: lighten($ui-base-color, 4%);
+  border-top: 1px solid lighten($ui-base-color, 8%);
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+
+  &__message {
+    position: relative;
+    margin-left: 58px;
+    color: $ui-base-lighter-color;
+    padding: 8px 0;
+    padding-top: 0;
+    padding-bottom: 4px;
+    font-size: 14px;
+
+    > span {
+      display: block;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+
+  &__icon-wrapper {
+    left: -26px;
+    position: absolute;
+  }
+
+  .detailed-status__display-avatar {
+    position: relative;
+  }
+
+  .detailed-status__display-name {
+    margin-bottom: 0;
+  }
+}
diff --git a/app/javascript/styles/mastodon/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss
index 0bf9daafd..2f7c98d54 100644
--- a/app/javascript/styles/mastodon/landing_strip.scss
+++ b/app/javascript/styles/mastodon/landing_strip.scss
@@ -34,3 +34,66 @@
 .memoriam-strip {
   background: rgba($base-shadow-color, 0.7);
 }
+
+.moved-strip {
+  padding: 14px;
+  border-radius: 4px;
+  background: rgba(darken($ui-base-color, 7%), 0.8);
+  color: $ui-secondary-color;
+  font-weight: 400;
+  margin-bottom: 20px;
+
+  strong,
+  a {
+    font-weight: 500;
+  }
+
+  a {
+    color: inherit;
+    text-decoration: underline;
+
+    &.mention {
+      text-decoration: none;
+
+      span {
+        text-decoration: none;
+      }
+
+      &:focus,
+      &:hover,
+      &:active {
+        text-decoration: none;
+
+        span {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+
+  &__message {
+    margin-bottom: 15px;
+
+    .fa {
+      margin-right: 5px;
+      color: $ui-primary-color;
+    }
+  }
+
+  &__card {
+    .detailed-status__display-avatar {
+      position: relative;
+      cursor: pointer;
+    }
+
+    .detailed-status__display-name {
+      margin-bottom: 0;
+      text-decoration: none;
+
+      span {
+        color: $ui-highlight-color;
+        font-weight: 400;
+      }
+    }
+  }
+}
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 66e4f7c5e..31e0abe39 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -1,6 +1,9 @@
 # frozen_string_literal: true
 
 class ActivityPub::Activity::Create < ActivityPub::Activity
+  SUPPORTED_TYPES = %w(Article Note).freeze
+  CONVERTED_TYPES = %w(Image Video).freeze
+
   def perform
     return if delete_arrived_first?(object_uri) || unsupported_object_type?
 
@@ -41,7 +44,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
       url: object_url || @object['id'],
       account: @account,
       text: text_from_content || '',
-      language: language_from_content,
+      language: detected_language,
       spoiler_text: @object['summary'] || '',
       created_at: @options[:override_timestamps] ? nil : @object['published'],
       reply: @object['inReplyTo'].present?,
@@ -165,40 +168,62 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   end
 
   def text_from_content
+    return Formatter.instance.linkify([text_from_name, object_url || @object['id']].join(' ')) if converted_object_type?
+
     if @object['content'].present?
       @object['content']
-    elsif language_map?
+    elsif content_language_map?
       @object['contentMap'].values.first
     end
   end
 
-  def language_from_content
-    return LanguageDetector.instance.detect(text_from_content, @account) unless language_map?
-    @object['contentMap'].keys.first
+  def text_from_name
+    if @object['name'].present?
+      @object['name']
+    elsif name_language_map?
+      @object['nameMap'].values.first
+    end
+  end
+
+  def detected_language
+    if content_language_map?
+      @object['contentMap'].keys.first
+    elsif name_language_map?
+      @object['nameMap'].keys.first
+    elsif supported_object_type?
+      LanguageDetector.instance.detect(text_from_content, @account)
+    end
   end
 
   def object_url
     return if @object['url'].blank?
-
-    value = first_of_value(@object['url'])
-
-    return value if value.is_a?(String)
-
-    value['href']
+    url_to_href(@object['url'], 'text/html')
   end
 
-  def language_map?
+  def content_language_map?
     @object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty?
   end
 
+  def name_language_map?
+    @object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
+  end
+
   def unsupported_object_type?
-    @object.is_a?(String) || !%w(Article Note).include?(@object['type'])
+    @object.is_a?(String) || !(supported_object_type? || converted_object_type?)
   end
 
   def unsupported_media_type?(mime_type)
     mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
   end
 
+  def supported_object_type?
+    SUPPORTED_TYPES.include?(@object['type'])
+  end
+
+  def converted_object_type?
+    CONVERTED_TYPES.include?(@object['type'])
+  end
+
   def skip_download?
     return @skip_download if defined?(@skip_download)
     @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
@@ -210,7 +235,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 
   def forward_for_reply
     return unless @json['signature'].present? && reply_to_local?
-    ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id)
+    ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
   end
 
   def lock_options
diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb
index 4c6afb090..d0fb49342 100644
--- a/app/lib/activitypub/activity/delete.rb
+++ b/app/lib/activitypub/activity/delete.rb
@@ -30,8 +30,11 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
   def forward_for_reblogs(status)
     return if @json['signature'].blank?
 
-    ActivityPub::RawDistributionWorker.push_bulk(status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)) do |account_id|
-      [payload, account_id]
+    rebloggers_ids = status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)
+    inboxes        = Account.where(id: ::Follow.where(target_account_id: rebloggers_ids).select(:account_id)).inboxes - [@account.preferred_inbox_url]
+
+    ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
+      [payload, rebloggers_ids.first, inbox_url]
     end
   end
 
diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb
index 790d2025c..90d589d90 100644
--- a/app/lib/activitypub/adapter.rb
+++ b/app/lib/activitypub/adapter.rb
@@ -9,6 +9,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
       {
         'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
         'sensitive'                 => 'as:sensitive',
+        'movedTo'                   => 'as:movedTo',
         'Hashtag'                   => 'as:Hashtag',
         'ostatus'                   => 'http://ostatus.org#',
         'atomUri'                   => 'ostatus:atomUri',
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 76365c7d3..fe5ebfc36 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -149,7 +149,7 @@ class FeedManager
     return false if receiver_id == status.account_id
     return true  if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
 
-    return true if keyword_filter?(status, Glitch::KeywordMute.matcher_for(receiver_id))
+    return true if keyword_filter?(status, receiver_id)
 
     check_for_mutes = [status.account_id]
     check_for_mutes.concat(status.mentions.pluck(:account_id))
@@ -162,32 +162,38 @@ class FeedManager
 
     return true if Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any?
 
-    if status.reply? && !status.in_reply_to_account_id.nil?                                                              # Filter out if it's a reply
-      should_filter   = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists? # and I'm not following the person it's a reply to
-      should_filter &&= receiver_id != status.in_reply_to_account_id                                                     # and it's not a reply to me
-      should_filter &&= status.account_id != status.in_reply_to_account_id                                               # and it's not a self-reply
+    if status.reply? && !status.in_reply_to_account_id.nil?                                                                      # Filter out if it's a reply
+      should_filter   = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists?         # and I'm not following the person it's a reply to
+      should_filter &&= receiver_id != status.in_reply_to_account_id                                                             # and it's not a reply to me
+      should_filter &&= status.account_id != status.in_reply_to_account_id                                                       # and it's not a self-reply
       return should_filter
-    elsif status.reblog?                                                                                                 # Filter out a reblog
-      src_id = status.account_id
-      should_filter   = Follow.where(account_id: receiver_id, target_account_id: src_id, show_reblogs: false).exists?    # if the reblogger's reblogs are suppressed
-      should_filter ||= Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists?        # or if the author of the reblogged status is blocking me
-      should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists?  # or the author's domain is blocked
+    elsif status.reblog?                                                                                                         # Filter out a reblog
+      should_filter   = Follow.where(account_id: receiver_id, target_account_id: status.account_id, show_reblogs: false).exists? # if the reblogger's reblogs are suppressed
+      should_filter ||= Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists?                # or if the author of the reblogged status is blocking me
+      should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists?          # or the author's domain is blocked
       return should_filter
     end
 
     false
   end
 
-  def keyword_filter?(status, matcher)
-    should_filter   = matcher =~ status.text
-    should_filter ||= matcher =~ status.spoiler_text
+  def keyword_filter?(status, receiver_id)
+    text_matcher = Glitch::KeywordMute.text_matcher_for(receiver_id)
+    tag_matcher  = Glitch::KeywordMute.tag_matcher_for(receiver_id)
+
+    should_filter   = text_matcher.matches?(status.text)
+    should_filter ||= text_matcher.matches?(status.spoiler_text)
+    should_filter ||= tag_matcher.matches?(status.tags)
 
     if status.reblog?
-      should_filter ||= matcher =~ status.reblog.text
-      should_filter ||= matcher =~ status.reblog.spoiler_text
+      reblog = status.reblog
+
+      should_filter ||= text_matcher.matches?(reblog.text)
+      should_filter ||= text_matcher.matches?(reblog.spoiler_text)
+      should_filter ||= tag_matcher.matches?(status.tags)
     end
 
-    !!should_filter
+    should_filter
   end
 
   def filter_from_mentions?(status, receiver_id)
@@ -199,7 +205,7 @@ class FeedManager
 
     should_filter   = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any?                                     # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked
     should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them
-    should_filter ||= keyword_filter?(status, Glitch::KeywordMute.matcher_for(receiver_id))                                              # or if the mention contains a muted keyword
+    should_filter ||= keyword_filter?(status, receiver_id)                                                                               # or if the mention contains a muted keyword
 
     should_filter
   end
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 733a1c4b7..9d8bc52db 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -51,12 +51,7 @@ class Formatter
 
   def simplified_format(account)
     return reformat(account.note).html_safe unless account.local? # rubocop:disable Rails/OutputSafety
-
-    html = encode_and_link_urls(account.note)
-    html = simple_format(html, {}, sanitize: false)
-    html = html.delete("\n")
-
-    html.html_safe # rubocop:disable Rails/OutputSafety
+    linkify(account.note)
   end
 
   def sanitize(html, config)
@@ -69,6 +64,14 @@ class Formatter
     html.html_safe # rubocop:disable Rails/OutputSafety
   end
 
+  def linkify(text)
+    html = encode_and_link_urls(text)
+    html = simple_format(html, {}, sanitize: false)
+    html = html.delete("\n")
+
+    html.html_safe # rubocop:disable Rails/OutputSafety
+  end
+
   private
 
   def encode(html)
diff --git a/app/models/account.rb b/app/models/account.rb
index a4b8e1c0b..ffd19fa52 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -42,6 +42,7 @@
 #  followers_url           :string           default(""), not null
 #  protocol                :integer          default("ostatus"), not null
 #  memorial                :boolean          default(FALSE), not null
+#  moved_to_account_id     :integer
 #
 
 class Account < ApplicationRecord
@@ -102,6 +103,9 @@ class Account < ApplicationRecord
   has_many :list_accounts, inverse_of: :account, dependent: :destroy
   has_many :lists, through: :list_accounts
 
+  # Account migrations
+  belongs_to :moved_to_account, class_name: 'Account'
+
   scope :remote, -> { where.not(domain: nil) }
   scope :local, -> { where(domain: nil) }
   scope :without_followers, -> { where(followers_count: 0) }
@@ -135,6 +139,10 @@ class Account < ApplicationRecord
     domain.nil?
   end
 
+  def moved?
+    moved_to_account_id.present?
+  end
+
   def acct
     local? ? username : "#{username}@#{domain}"
   end
@@ -208,6 +216,10 @@ class Account < ApplicationRecord
     Rails.cache.fetch("exclude_domains_for:#{id}") { domain_blocks.pluck(:domain) }
   end
 
+  def preferred_inbox_url
+    shared_inbox_url.presence || inbox_url
+  end
+
   class << self
     def readonly_attributes
       super - %w(statuses_count following_count followers_count)
diff --git a/app/models/admin.rb b/app/models/admin.rb
new file mode 100644
index 000000000..d41d18449
--- /dev/null
+++ b/app/models/admin.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Admin
+  def self.table_name_prefix
+    'admin_'
+  end
+end
diff --git a/app/models/admin/action_log.rb b/app/models/admin/action_log.rb
new file mode 100644
index 000000000..4e950fbf7
--- /dev/null
+++ b/app/models/admin/action_log.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: admin_action_logs
+#
+#  id               :integer          not null, primary key
+#  account_id       :integer
+#  action           :string           default(""), not null
+#  target_type      :string
+#  target_id        :integer
+#  recorded_changes :text             default(""), not null
+#  created_at       :datetime         not null
+#  updated_at       :datetime         not null
+#
+
+class Admin::ActionLog < ApplicationRecord
+  serialize :recorded_changes
+
+  belongs_to :account, required: true
+  belongs_to :target, required: true, polymorphic: true
+
+  default_scope -> { order('id desc') }
+
+  def action
+    super.to_sym
+  end
+
+  before_validation :set_changes
+
+  private
+
+  def set_changes
+    case action
+    when :destroy, :create
+      self.recorded_changes = target.attributes
+    when :update, :promote, :demote
+      self.recorded_changes = target.previous_changes
+    end
+  end
+end
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index c41f92581..fdf35a4e3 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -7,7 +7,7 @@ module AccountInteractions
     def following_map(target_account_ids, account_id)
       Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
         mapping[follow.target_account_id] = {
-          reblogs: follow.show_reblogs?
+          reblogs: follow.show_reblogs?,
         }
       end
     end
@@ -31,7 +31,7 @@ module AccountInteractions
     def requested_map(target_account_ids, account_id)
       FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
         mapping[follow_request.target_account_id] = {
-          reblogs: follow_request.show_reblogs?
+          reblogs: follow_request.show_reblogs?,
         }
       end
     end
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index 2b148c82b..c1d2cf420 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -24,8 +24,12 @@ class Form::AdminSettings
     :open_deletion=,
     :timeline_preview,
     :timeline_preview=,
+    :show_staff_badge,
+    :show_staff_badge=,
     :bootstrap_timeline_accounts,
     :bootstrap_timeline_accounts=,
+    :min_invite_role,
+    :min_invite_role=,
     to: Setting
   )
 end
diff --git a/app/models/form/migration.rb b/app/models/form/migration.rb
new file mode 100644
index 000000000..b74987337
--- /dev/null
+++ b/app/models/form/migration.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Form::Migration
+  include ActiveModel::Validations
+
+  attr_accessor :acct, :account
+
+  def initialize(attrs = {})
+    @account = attrs[:account]
+    @acct    = attrs[:account].acct unless @account.nil?
+    @acct    = attrs[:acct].gsub(/\A@/, '').strip unless attrs[:acct].nil?
+  end
+
+  def valid?
+    return false unless super
+    set_account
+    errors.empty?
+  end
+
+  private
+
+  def set_account
+    self.account = (ResolveRemoteAccountService.new.call(acct) if account.nil? && acct.present?)
+  end
+end
diff --git a/app/models/form/status_batch.rb b/app/models/form/status_batch.rb
index a97b4aa28..4f08a3049 100644
--- a/app/models/form/status_batch.rb
+++ b/app/models/form/status_batch.rb
@@ -2,8 +2,9 @@
 
 class Form::StatusBatch
   include ActiveModel::Model
+  include AccountableConcern
 
-  attr_accessor :status_ids, :action
+  attr_accessor :status_ids, :action, :current_account
 
   ACTION_TYPE = %w(nsfw_on nsfw_off delete).freeze
 
@@ -20,11 +21,14 @@ class Form::StatusBatch
 
   def change_sensitive(sensitive)
     media_attached_status_ids = MediaAttachment.where(status_id: status_ids).pluck(:status_id)
+
     ApplicationRecord.transaction do
       Status.where(id: media_attached_status_ids).find_each do |status|
         status.update!(sensitive: sensitive)
+        log_action :update, status
       end
     end
+
     true
   rescue ActiveRecord::RecordInvalid
     false
@@ -33,7 +37,9 @@ class Form::StatusBatch
   def delete_statuses
     Status.where(id: status_ids).find_each do |status|
       RemovalWorker.perform_async(status.id)
+      log_action :destroy, status
     end
+
     true
   end
 end
diff --git a/app/models/glitch/keyword_mute.rb b/app/models/glitch/keyword_mute.rb
index 009de1880..a2481308f 100644
--- a/app/models/glitch/keyword_mute.rb
+++ b/app/models/glitch/keyword_mute.rb
@@ -16,51 +16,85 @@ class Glitch::KeywordMute < ApplicationRecord
 
   validates_presence_of :keyword
 
-  after_commit :invalidate_cached_matcher
+  after_commit :invalidate_cached_matchers
 
-  def self.matcher_for(account_id)
-    Matcher.new(account_id)
+  def self.text_matcher_for(account_id)
+    TextMatcher.new(account_id)
+  end
+
+  def self.tag_matcher_for(account_id)
+    TagMatcher.new(account_id)
   end
 
   private
 
-  def invalidate_cached_matcher
-    Rails.cache.delete("keyword_mutes:regex:#{account_id}")
+  def invalidate_cached_matchers
+    Rails.cache.delete(TextMatcher.cache_key(account_id))
+    Rails.cache.delete(TagMatcher.cache_key(account_id))
   end
 
-  class Matcher
+  class RegexpMatcher
     attr_reader :account_id
     attr_reader :regex
 
     def initialize(account_id)
       @account_id = account_id
-      regex_text = Rails.cache.fetch("keyword_mutes:regex:#{account_id}") { regex_text_for_account }
+      regex_text = Rails.cache.fetch(self.class.cache_key(account_id)) { make_regex_text }
       @regex = /#{regex_text}/
     end
 
-    def =~(str)
-      regex =~ str
+    protected
+
+    def keywords
+      Glitch::KeywordMute.where(account_id: account_id).pluck(:whole_word, :keyword)
     end
 
-    private
+    def boundary_regex_for_keyword(keyword)
+      sb = keyword =~ /\A[[:word:]]/ ? '\b' : ''
+      eb = keyword =~ /[[:word:]]\Z/ ? '\b' : ''
 
-    def keywords
-      Glitch::KeywordMute.where(account_id: account_id).select(:keyword, :id, :whole_word)
+      /(?mix:#{sb}#{Regexp.escape(keyword)}#{eb})/
+    end
+  end
+
+  class TextMatcher < RegexpMatcher
+    def self.cache_key(account_id)
+      format('keyword_mutes:regex:text:%s', account_id)
+    end
+
+    def matches?(str)
+      !!(regex =~ str)
     end
 
-    def regex_text_for_account
-      kws = keywords.find_each.with_object([]) do |kw, a|
-        a << (kw.whole_word ? boundary_regex_for_keyword(kw.keyword) : kw.keyword)
+    private
+
+    def make_regex_text
+      kws = keywords.map! do |whole_word, keyword|
+        whole_word ? boundary_regex_for_keyword(keyword) : keyword
       end
 
       Regexp.union(kws).source
     end
+  end
 
-    def boundary_regex_for_keyword(keyword)
-      sb = keyword =~ /\A[[:word:]]/ ? '\b' : ''
-      eb = keyword =~ /[[:word:]]\Z/ ? '\b' : ''
+  class TagMatcher < RegexpMatcher
+    def self.cache_key(account_id)
+      format('keyword_mutes:regex:tag:%s', account_id)
+    end
 
-      /(?mix:#{sb}#{Regexp.escape(keyword)}#{eb})/
+    def matches?(tags)
+      tags.pluck(:name).any? { |n| regex =~ n }
+    end
+
+    private
+
+    def make_regex_text
+      kws = keywords.map! do |whole_word, keyword|
+        term = (Tag::HASHTAG_RE =~ keyword) ? $1 : keyword
+        whole_word ? boundary_regex_for_keyword(term) : term
+      end
+
+      Regexp.union(kws).source
     end
   end
 end
diff --git a/app/models/invite.rb b/app/models/invite.rb
new file mode 100644
index 000000000..6907c1f1d
--- /dev/null
+++ b/app/models/invite.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: invites
+#
+#  id         :integer          not null, primary key
+#  user_id    :integer
+#  code       :string           default(""), not null
+#  expires_at :datetime
+#  max_uses   :integer
+#  uses       :integer          default(0), not null
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class Invite < ApplicationRecord
+  belongs_to :user, required: true
+  has_many :users, inverse_of: :invite
+
+  scope :available, -> { where(expires_at: nil).or(where('expires_at >= ?', Time.now.utc)) }
+  scope :expired, -> { where.not(expires_at: nil).where('expires_at < ?', Time.now.utc) }
+
+  before_validation :set_code
+
+  attr_reader :expires_in
+
+  def expires_in=(interval)
+    self.expires_at = interval.to_i.seconds.from_now unless interval.blank?
+    @expires_in     = interval
+  end
+
+  def valid_for_use?
+    (max_uses.nil? || uses < max_uses) && !expired?
+  end
+
+  def expire!
+    touch(:expires_at)
+  end
+
+  def expired?
+    !expires_at.nil? && expires_at < Time.now.utc
+  end
+
+  private
+
+  def set_code
+    loop do
+      self.code = ([*('a'..'z'), *('A'..'Z'), *('0'..'9')] - %w(0 1 I l O)).sample(8).join
+      break if Invite.find_by(code: code).nil?
+    end
+  end
+end
diff --git a/app/models/invite_filter.rb b/app/models/invite_filter.rb
new file mode 100644
index 000000000..7d89bad4a
--- /dev/null
+++ b/app/models/invite_filter.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class InviteFilter
+  attr_reader :params
+
+  def initialize(params)
+    @params = params
+  end
+
+  def results
+    scope = Invite.order(created_at: :desc)
+
+    params.each do |key, value|
+      scope.merge!(scope_for(key, value)) if value.present?
+    end
+
+    scope
+  end
+
+  private
+
+  def scope_for(key, _value)
+    case key.to_s
+    when 'available'
+      Invite.available
+    when 'expired'
+      Invite.expired
+    else
+      raise "Unknown filter: #{key}"
+    end
+  end
+end
diff --git a/app/models/notification.rb b/app/models/notification.rb
index a3ffb1f45..976963528 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -24,7 +24,7 @@ class Notification < ApplicationRecord
     favourite:      'Favourite',
   }.freeze
 
-  STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze
+  STATUS_INCLUDES = [:account, :application, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :application, :media_attachments, :tags, mentions: :account]].freeze
 
   belongs_to :account
   belongs_to :from_account, class_name: 'Account'
@@ -55,9 +55,11 @@ class Notification < ApplicationRecord
   def target_status
     case type
     when :reblog
-      activity&.reblog
-    when :favourite, :mention
-      activity&.status
+      status&.reblog
+    when :favourite
+      favourite&.status
+    when :mention
+      mention&.status
     end
   end
 
diff --git a/app/models/status.rb b/app/models/status.rb
index 172d3a665..70cfdc1c7 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -183,7 +183,7 @@ class Status < ApplicationRecord
     end
 
     def reblogs_map(status_ids, account_id)
-      select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).map { |s| [s.reblog_of_id, true] }.to_h
+      select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).reorder(nil).map { |s| [s.reblog_of_id, true] }.to_h
     end
 
     def mutes_map(conversation_ids, account_id)
@@ -291,6 +291,7 @@ class Status < ApplicationRecord
 
   def set_visibility
     self.visibility = (account.locked? ? :private : :public) if visibility.nil?
+    self.visibility = reblog.visibility if reblog?
     self.sensitive  = false if sensitive.nil?
   end
 
diff --git a/app/models/user.rb b/app/models/user.rb
index 2458cf298..29bdcbd67 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -33,6 +33,7 @@
 #  account_id                :integer          not null
 #  disabled                  :boolean          default(FALSE), not null
 #  moderator                 :boolean          default(FALSE), not null
+#  invite_id                 :integer
 #
 
 class User < ApplicationRecord
@@ -47,6 +48,7 @@ class User < ApplicationRecord
          otp_number_of_backup_codes: 10
 
   belongs_to :account, inverse_of: :user, required: true
+  belongs_to :invite, counter_cache: :uses
   accepts_nested_attributes_for :account
 
   has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
@@ -77,6 +79,8 @@ class User < ApplicationRecord
            :reduce_motion, :system_font_ui, :noindex, :flavour, :skin,
            to: :settings, prefix: :setting, allow_nil: false
 
+  attr_accessor :invite_code
+
   def confirmed?
     confirmed_at.present?
   end
@@ -95,6 +99,19 @@ class User < ApplicationRecord
     end
   end
 
+  def role?(role)
+    case role
+    when 'user'
+      true
+    when 'moderator'
+      staff?
+    when 'admin'
+      admin?
+    else
+      false
+    end
+  end
+
   def disable!
     update!(disabled: true,
             last_sign_in_at: current_sign_in_at,
@@ -169,6 +186,11 @@ class User < ApplicationRecord
     session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload
   end
 
+  def invite_code=(code)
+    self.invite  = Invite.find_by(code: code) unless code.blank?
+    @invite_code = code
+  end
+
   protected
 
   def send_devise_notification(notification, *args)
diff --git a/app/policies/invite_policy.rb b/app/policies/invite_policy.rb
new file mode 100644
index 000000000..a2a65f934
--- /dev/null
+++ b/app/policies/invite_policy.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class InvitePolicy < ApplicationPolicy
+  def index?
+    staff?
+  end
+
+  def create?
+    min_required_role?
+  end
+
+  def destroy?
+    owner? || (Setting.min_invite_role == 'admin' ? admin? : staff?)
+  end
+
+  private
+
+  def owner?
+    record.user_id == current_user&.id
+  end
+
+  def min_required_role?
+    current_user&.role?(Setting.min_invite_role)
+  end
+end
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index 896d67115..622bdde0c 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -10,6 +10,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
 
   has_one :public_key, serializer: ActivityPub::PublicKeySerializer
 
+  attribute :moved_to, if: :moved?
+
   class EndpointsSerializer < ActiveModel::Serializer
     include RoutingHelper
 
@@ -25,6 +27,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
   has_one :icon,  serializer: ActivityPub::ImageSerializer, if: :avatar_exists?
   has_one :image, serializer: ActivityPub::ImageSerializer, if: :header_exists?
 
+  delegate :moved?, to: :object
+
   def id
     account_url(object)
   end
@@ -92,4 +96,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
   def manually_approves_followers
     object.locked
   end
+
+  def moved_to
+    ActivityPub::TagManager.instance.uri_for(object.moved_to_account)
+  end
 end
diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb
index 65fdb0308..bab944c5a 100644
--- a/app/serializers/rest/account_serializer.rb
+++ b/app/serializers/rest/account_serializer.rb
@@ -7,6 +7,10 @@ class REST::AccountSerializer < ActiveModel::Serializer
              :note, :url, :avatar, :avatar_static, :header, :header_static,
              :followers_count, :following_count, :statuses_count
 
+  has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved?
+
+  delegate :moved?, to: :object
+
   def id
     object.id.to_s
   end
diff --git a/app/serializers/rest/list_serializer.rb b/app/serializers/rest/list_serializer.rb
index c0150888e..977da7439 100644
--- a/app/serializers/rest/list_serializer.rb
+++ b/app/serializers/rest/list_serializer.rb
@@ -2,4 +2,8 @@
 
 class REST::ListSerializer < ActiveModel::Serializer
   attributes :id, :title
+
+  def id
+    object.id.to_s
+  end
 end
diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb
index 8d7b7a17c..7649bceca 100644
--- a/app/services/activitypub/fetch_remote_status_service.rb
+++ b/app/services/activitypub/fetch_remote_status_service.rb
@@ -18,7 +18,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
     actor = ActivityPub::TagManager.instance.uri_to_resource(actor_id, Account)
     actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id, id: true) if actor.nil? || needs_update(actor)
 
-    return if actor.suspended?
+    return if actor.nil? || actor.suspended?
 
     ActivityPub::Activity.factory(activity_json, actor).perform
   end
@@ -42,7 +42,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
   end
 
   def expected_type?
-    %w(Note Article).include? @json['type']
+    (ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES).include? @json['type']
   end
 
   def needs_update(actor)
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index f93baf4b5..06ca75563 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -74,6 +74,7 @@ class ActivityPub::ProcessAccountService < BaseService
     @account.statuses_count    = outbox_total_items    if outbox_total_items.present?
     @account.following_count   = following_total_items if following_total_items.present?
     @account.followers_count   = followers_total_items if followers_total_items.present?
+    @account.moved_to_account  = moved_account         if @json['movedTo'].present?
   end
 
   def after_protocol_change!
@@ -106,12 +107,7 @@ class ActivityPub::ProcessAccountService < BaseService
 
   def url
     return if @json['url'].blank?
-
-    value = first_of_value(@json['url'])
-
-    return value if value.is_a?(String)
-
-    value['href']
+    url_to_href(@json['url'], 'text/html')
   end
 
   def outbox_total_items
@@ -137,6 +133,12 @@ class ActivityPub::ProcessAccountService < BaseService
     @collections[type] = nil
   end
 
+  def moved_account
+    account   = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account)
+    account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true)
+    account
+  end
+
   def skip_download?
     @account.suspended? || domain_block&.reject_media?
   end
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 14c21b6cc..37cf75379 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -74,6 +74,9 @@ class FetchLinkCardService < BaseService
 
     return false unless response.respond_to?(:type)
 
+    # The photo will change the URL. So, to avoid duplication of URLs, PreviewCard needs to be checked again.
+    @card = PreviewCard.find_by(url: response.url) || @card if response.type == 'photo'
+
     @card.type          = response.type
     @card.title         = response.respond_to?(:title)         ? response.title         : ''
     @card.author_name   = response.respond_to?(:author_name)   ? response.author_name   : ''
diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb
index a229d4ff8..e12721c46 100644
--- a/app/services/process_mentions_service.rb
+++ b/app/services/process_mentions_service.rb
@@ -18,7 +18,7 @@ class ProcessMentionsService < BaseService
       end
 
       if mentioned_account.nil?
-        username, domain  = match.first.split('@')
+        username, domain  = $1.split('@')
         mentioned_account = Account.find_remote(username, domain)
       end
 
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 9617081fd..7789bd441 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -3,7 +3,7 @@
 class RemoveStatusService < BaseService
   include StreamEntryRenderer
 
-  def call(status)
+  def call(status, options = {})
     @payload      = Oj.dump(event: :delete, payload: status.id.to_s)
     @status       = status
     @account      = status.account
@@ -11,6 +11,7 @@ class RemoveStatusService < BaseService
     @mentions     = status.mentions.includes(:account).to_a
     @reblogs      = status.reblogs.to_a
     @stream_entry = status.stream_entry
+    @options      = options
 
     remove_from_self if status.account.local?
     remove_from_followers
@@ -23,7 +24,12 @@ class RemoveStatusService < BaseService
 
     @status.destroy!
 
-    return unless @account.local?
+    # There is no reason to send out Undo activities when the
+    # cause is that the original object has been removed, since
+    # original object being removed implicitly removes reblogs
+    # of it. The Delete activity of the original is forwarded
+    # separately.
+    return if !@account.local? || @options[:original_removed]
 
     remove_from_remote_followers
     remove_from_remote_affected
@@ -105,7 +111,7 @@ class RemoveStatusService < BaseService
     # without us being able to do all the fancy stuff
 
     @reblogs.each do |reblog|
-      RemoveStatusService.new.call(reblog)
+      RemoveStatusService.new.call(reblog, original_removed: true)
     end
   end
 
diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml
index 94ec5ae5b..b0062752c 100644
--- a/app/views/accounts/_header.html.haml
+++ b/app/views/accounts/_header.html.haml
@@ -1,7 +1,7 @@
 - processed_bio = FrontmatterHandler.instance.process_bio Formatter.instance.simplified_format account
 .card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" }
   .card__illustration
-    - unless account.memorial?
+    - unless account.memorial? || account.moved?
       - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account)
         .controls
           - if current_account.following?(account)
@@ -27,15 +27,15 @@
       %small
         %span @#{account.local_username_and_domain}
         = fa_icon('lock') if account.locked?
-
-    - if account.user_admin?
-      .roles
-        .account-role.admin
-          = t 'accounts.roles.admin'
-    - elsif account.user_moderator?
-      .roles
-        .account-role.moderator
-          = t 'accounts.roles.moderator'
+    - if Setting.show_staff_badge
+      - if account.user_admin?
+        .roles
+          .account-role.admin
+            = t 'accounts.roles.admin'
+      - elsif account.user_moderator?
+        .roles
+          .account-role.moderator
+            = t 'accounts.roles.moderator'
     .bio
       .account__header__content.p-note.emojify!=processed_bio[:text]
       - if processed_bio[:metadata].length > 0
diff --git a/app/views/accounts/_moved_strip.html.haml b/app/views/accounts/_moved_strip.html.haml
new file mode 100644
index 000000000..6a14a5dd3
--- /dev/null
+++ b/app/views/accounts/_moved_strip.html.haml
@@ -0,0 +1,17 @@
+- moved_to_account = account.moved_to_account
+
+.moved-strip
+  .moved-strip__message
+    = fa_icon 'suitcase'
+    = t('accounts.moved_html', name: content_tag(:strong, display_name(account), class: :emojify), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), TagManager.instance.url_for(moved_to_account), class: 'mention'))
+
+  .moved-strip__card
+    = link_to TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do
+      .detailed-status__display-avatar
+        .account__avatar-overlay
+          .account__avatar-overlay-base{ style: "background-image: url('#{moved_to_account.avatar.url(:original)}')" }
+          .account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" }
+
+      %span.display-name
+        %strong.emojify= display_name(moved_to_account)
+        %span @#{moved_to_account.acct}
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index fd8ad5530..accad5f78 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -14,6 +14,8 @@
 
 - if @account.memorial?
   .memoriam-strip= t('in_memoriam_html')
+- elsif @account.moved?
+  = render partial: 'moved_strip', locals: { account: @account }
 - elsif show_landing_strip?
   = render partial: 'shared/landing_strip', locals: { account: @account }
 
diff --git a/app/views/admin/action_logs/_action_log.html.haml b/app/views/admin/action_logs/_action_log.html.haml
new file mode 100644
index 000000000..ec90961cb
--- /dev/null
+++ b/app/views/admin/action_logs/_action_log.html.haml
@@ -0,0 +1,15 @@
+%li.log-entry
+  .log-entry__header
+    .log-entry__avatar
+      = image_tag action_log.account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar'
+    .log-entry__content
+      .log-entry__title
+        = t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target')).html_safe
+      .log-entry__timestamp
+        %time= l action_log.created_at
+    .spacer
+    .log-entry__icon
+      = fa_icon icon_for_log(action_log)
+      .log-entry__icon__overlay{ class: class_for_log_icon(action_log) }
+  .log-entry__extras
+    = log_extra_attributes relevant_log_changes(action_log)
diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml
new file mode 100644
index 000000000..bb6d7b5d7
--- /dev/null
+++ b/app/views/admin/action_logs/index.html.haml
@@ -0,0 +1,7 @@
+- content_for :page_title do
+  = t('admin.action_logs.title')
+
+%ul
+  = render @action_logs
+
+= paginate @action_logs
diff --git a/app/views/admin/invites/_invite.html.haml b/app/views/admin/invites/_invite.html.haml
new file mode 100644
index 000000000..d7b697286
--- /dev/null
+++ b/app/views/admin/invites/_invite.html.haml
@@ -0,0 +1,21 @@
+%tr
+  %td
+    .name-tag
+      = image_tag invite.user.account.avatar.url(:original), alt: '', width: 16, height: 16, class: 'avatar'
+      %span.username= invite.user.account.username
+  %td
+    = invite.uses
+    = " / #{invite.max_uses}" unless invite.max_uses.nil?
+  %td
+    - if invite.expired?
+      = t('invites.expired')
+    - else
+      - if invite.expires_at.nil?
+        ∞
+      - else
+        %time.formatted{ datetime: invite.expires_at.iso8601, title: l(invite.expires_at) }
+          = l invite.expires_at
+  %td= table_link_to 'link', public_invite_url(invite_code: invite.code), public_invite_url(invite_code: invite.code)
+  %td
+    - if !invite.expired? && policy(invite).destroy?
+      = table_link_to 'times', t('invites.delete'), admin_invite_path(invite), method: :delete
diff --git a/app/views/admin/invites/index.html.haml b/app/views/admin/invites/index.html.haml
new file mode 100644
index 000000000..944a60471
--- /dev/null
+++ b/app/views/admin/invites/index.html.haml
@@ -0,0 +1,30 @@
+- content_for :page_title do
+  = t('admin.invites.title')
+
+.filters
+  .filter-subset
+    %strong= t('admin.invites.filter.title')
+    %ul
+      %li= filter_link_to t('admin.invites.filter.all'), available: nil, expired: nil
+      %li= filter_link_to t('admin.invites.filter.available'), available: 1, expired: nil
+      %li= filter_link_to t('admin.invites.filter.expired'), available: nil, expired: 1
+
+- if policy(:invite).create?
+  %p= t('invites.prompt')
+
+  = render 'invites/form'
+
+  %hr/
+
+%table.table
+  %thead
+    %tr
+      %th
+      %th= t('invites.table.uses')
+      %th= t('invites.table.expires_at')
+      %th
+      %th
+  %tbody
+    = render @invites
+
+= paginate @invites
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index 468166035..c7c25f528 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -19,6 +19,9 @@
     = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html')
 
   .fields-group
+    = f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html')
+
+  .fields-group
     = f.input :open_registrations, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.open.title'), hint: t('admin.settings.registrations.open.desc_html')
 
   .fields-group
@@ -30,6 +33,11 @@
   %hr/
 
   .fields-group
+    = f.input :min_invite_role, wrapper: :with_label, collection: %i(disabled user moderator admin), label: t('admin.settings.registrations.min_invite_role.title'), label_method: lambda { |role| role == :disabled ? t('admin.settings.registrations.min_invite_role.disabled') : t("admin.accounts.roles.#{role}") }, as: :radio_buttons, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
+
+  %hr/
+
+  .fields-group
     = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 }
     = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 }
 
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index f71675df0..2d4c0f5ac 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -16,6 +16,7 @@
   = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }
   = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
   = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
+  = f.input :invite_code, as: :hidden
 
   .actions
     = f.button :button, t('auth.register'), type: :submit
diff --git a/app/views/invites/_form.html.haml b/app/views/invites/_form.html.haml
new file mode 100644
index 000000000..a01cf5946
--- /dev/null
+++ b/app/views/invites/_form.html.haml
@@ -0,0 +1,9 @@
+= simple_form_for(@invite, url: controller.is_a?(Admin::InvitesController) ? admin_invites_path : invites_path) do |f|
+  = render 'shared/error_messages', object: @invite
+
+  .fields-group
+    = f.input :max_uses, wrapper: :with_label, collection: [1, 5, 10, 25, 50, 100], label_method: lambda { |num| I18n.t('invites.max_uses', count: num) }, prompt: I18n.t('invites.max_uses_prompt')
+    = f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
+
+  .actions
+    = f.button :button, t('invites.generate'), type: :submit
diff --git a/app/views/invites/_invite.html.haml b/app/views/invites/_invite.html.haml
new file mode 100644
index 000000000..81d67eb7d
--- /dev/null
+++ b/app/views/invites/_invite.html.haml
@@ -0,0 +1,17 @@
+%tr
+  %td
+    = invite.uses
+    = " / #{invite.max_uses}" unless invite.max_uses.nil?
+  %td
+    - if invite.expired?
+      = t('invites.expired')
+    - else
+      - if invite.expires_at.nil?
+        ∞
+      - else
+        %time.formatted{ datetime: invite.expires_at.iso8601, title: l(invite.expires_at) }
+          = l invite.expires_at
+  %td= table_link_to 'link', public_invite_url(invite_code: invite.code), public_invite_url(invite_code: invite.code)
+  %td
+    - if invite.expired? && policy(invite).destroy?
+      = table_link_to 'times', t('invites.delete'), invite_path(invite), method: :delete
diff --git a/app/views/invites/index.html.haml b/app/views/invites/index.html.haml
new file mode 100644
index 000000000..f4c5047fa
--- /dev/null
+++ b/app/views/invites/index.html.haml
@@ -0,0 +1,19 @@
+- content_for :page_title do
+  = t('invites.title')
+
+- if policy(:invite).create?
+  %p= t('invites.prompt')
+
+  = render 'form'
+
+  %hr/
+
+%table.table
+  %thead
+    %tr
+      %th= t('invites.table.uses')
+      %th= t('invites.table.expires_at')
+      %th
+      %th
+  %tbody
+    = render @invites
diff --git a/app/views/settings/migrations/show.html.haml b/app/views/settings/migrations/show.html.haml
new file mode 100644
index 000000000..b7c34761f
--- /dev/null
+++ b/app/views/settings/migrations/show.html.haml
@@ -0,0 +1,17 @@
+- content_for :page_title do
+  = t('settings.migrate')
+
+= simple_form_for @migration, as: :migration, url: settings_migration_path, html: { method: :put } do |f|
+  - if @migration.account
+    %p.hint= t('migrations.currently_redirecting')
+
+    .fields-group
+      = render partial: 'authorize_follows/card', locals: { account: @migration.account }
+
+  = render 'shared/error_messages', object: @migration
+
+  .fields-group
+    = f.input :acct, placeholder: t('migrations.acct')
+
+  .actions
+    = f.button :button, t('migrations.proceed'), type: :submit, class: 'negative'
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index 551a7ca49..be7bd0ba0 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -21,3 +21,8 @@
 
   .actions
     = f.button :button, t('generic.save_changes'), type: :submit
+
+%hr/
+
+%h6= t('auth.migrate_account')
+%p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path)
diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml
index 428069931..895a61247 100644
--- a/app/views/stream_entries/show.html.haml
+++ b/app/views/stream_entries/show.html.haml
@@ -8,7 +8,7 @@
 
   = opengraph 'og:site_name', site_title
   = opengraph 'og:type', 'article'
-  = opengraph 'og:title', "#{@account.username} on #{site_hostname}"
+  = opengraph 'og:title', "#{@account.display_name.presence || @account.username} on #{site_hostname}"
   = opengraph 'og:url', account_stream_entry_url(@account, @stream_entry)
 
   = render 'stream_entries/og_description', activity: @stream_entry.activity
diff --git a/app/workers/activitypub/raw_distribution_worker.rb b/app/workers/activitypub/raw_distribution_worker.rb
index d73466f6e..41e61132f 100644
--- a/app/workers/activitypub/raw_distribution_worker.rb
+++ b/app/workers/activitypub/raw_distribution_worker.rb
@@ -5,10 +5,10 @@ class ActivityPub::RawDistributionWorker
 
   sidekiq_options queue: 'push'
 
-  def perform(json, source_account_id)
+  def perform(json, source_account_id, exclude_inboxes = [])
     @account = Account.find(source_account_id)
 
-    ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
+    ActivityPub::DeliveryWorker.push_bulk(inboxes - exclude_inboxes) do |inbox_url|
       [json, @account.id, inbox_url]
     end
   rescue ActiveRecord::RecordNotFound
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
index f7cf89dff..db7e37bb9 100644
--- a/config/brakeman.ignore
+++ b/config/brakeman.ignore
@@ -7,10 +7,10 @@
       "check_name": "LinkToHref",
       "message": "Potentially unsafe model attribute in link_to href",
       "file": "app/views/admin/accounts/show.html.haml",
-      "line": 122,
+      "line": 143,
       "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
       "code": "link_to(Account.find(params[:id]).inbox_url, Account.find(params[:id]).inbox_url)",
-      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
       "location": {
         "type": "template",
         "template": "admin/accounts/show"
@@ -26,10 +26,10 @@
       "check_name": "LinkToHref",
       "message": "Potentially unsafe model attribute in link_to href",
       "file": "app/views/admin/accounts/show.html.haml",
-      "line": 128,
+      "line": 149,
       "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
       "code": "link_to(Account.find(params[:id]).shared_inbox_url, Account.find(params[:id]).shared_inbox_url)",
-      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
       "location": {
         "type": "template",
         "template": "admin/accounts/show"
@@ -45,10 +45,10 @@
       "check_name": "LinkToHref",
       "message": "Potentially unsafe model attribute in link_to href",
       "file": "app/views/admin/accounts/show.html.haml",
-      "line": 35,
+      "line": 54,
       "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
       "code": "link_to(Account.find(params[:id]).url, Account.find(params[:id]).url)",
-      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
       "location": {
         "type": "template",
         "template": "admin/accounts/show"
@@ -77,16 +77,35 @@
       "note": ""
     },
     {
+      "warning_type": "Dynamic Render Path",
+      "warning_code": 15,
+      "fingerprint": "4b6a895e2805578d03ceedbe1d469cc75a0c759eba093722523edb4b8683c873",
+      "check_name": "Render",
+      "message": "Render path contains parameter value",
+      "file": "app/views/admin/action_logs/index.html.haml",
+      "line": 5,
+      "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
+      "code": "render(action => Admin::ActionLog.page(params[:page]), {})",
+      "render_path": [{"type":"controller","class":"Admin::ActionLogsController","method":"index","line":7,"file":"app/controllers/admin/action_logs_controller.rb"}],
+      "location": {
+        "type": "template",
+        "template": "admin/action_logs/index"
+      },
+      "user_input": "params[:page]",
+      "confidence": "Weak",
+      "note": ""
+    },
+    {
       "warning_type": "Cross-Site Scripting",
       "warning_code": 4,
       "fingerprint": "64b5b2a02ede9c2b3598881eb5a466d63f7d27fe0946aa00d570111ec7338d2e",
       "check_name": "LinkToHref",
       "message": "Potentially unsafe model attribute in link_to href",
       "file": "app/views/admin/accounts/show.html.haml",
-      "line": 131,
+      "line": 152,
       "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
       "code": "link_to(Account.find(params[:id]).followers_url, Account.find(params[:id]).followers_url)",
-      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
       "location": {
         "type": "template",
         "template": "admin/accounts/show"
@@ -102,10 +121,10 @@
       "check_name": "LinkToHref",
       "message": "Potentially unsafe model attribute in link_to href",
       "file": "app/views/admin/accounts/show.html.haml",
-      "line": 106,
+      "line": 127,
       "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
       "code": "link_to(Account.find(params[:id]).salmon_url, Account.find(params[:id]).salmon_url)",
-      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
       "location": {
         "type": "template",
         "template": "admin/accounts/show"
@@ -124,7 +143,7 @@
       "line": 31,
       "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
       "code": "render(action => filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]), {})",
-      "render_path": [{"type":"controller","class":"Admin::CustomEmojisController","method":"index","line":9,"file":"app/controllers/admin/custom_emojis_controller.rb"}],
+      "render_path": [{"type":"controller","class":"Admin::CustomEmojisController","method":"index","line":10,"file":"app/controllers/admin/custom_emojis_controller.rb"}],
       "location": {
         "type": "template",
         "template": "admin/custom_emojis/index"
@@ -163,7 +182,7 @@
       "line": 64,
       "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
       "code": "render(action => filtered_accounts.page(params[:page]), {})",
-      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"index","line":10,"file":"app/controllers/admin/accounts_controller.rb"}],
+      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"index","line":12,"file":"app/controllers/admin/accounts_controller.rb"}],
       "location": {
         "type": "template",
         "template": "admin/accounts/index"
@@ -179,10 +198,10 @@
       "check_name": "LinkToHref",
       "message": "Potentially unsafe model attribute in link_to href",
       "file": "app/views/admin/accounts/show.html.haml",
-      "line": 95,
+      "line": 116,
       "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
       "code": "link_to(Account.find(params[:id]).remote_url, Account.find(params[:id]).remote_url)",
-      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
       "location": {
         "type": "template",
         "template": "admin/accounts/show"
@@ -221,7 +240,7 @@
       "line": 25,
       "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
       "code": "render(action => filtered_reports.page(params[:page]), {})",
-      "render_path": [{"type":"controller","class":"Admin::ReportsController","method":"index","line":9,"file":"app/controllers/admin/reports_controller.rb"}],
+      "render_path": [{"type":"controller","class":"Admin::ReportsController","method":"index","line":10,"file":"app/controllers/admin/reports_controller.rb"}],
       "location": {
         "type": "template",
         "template": "admin/reports/index"
@@ -237,10 +256,10 @@
       "check_name": "LinkToHref",
       "message": "Potentially unsafe model attribute in link_to href",
       "file": "app/views/admin/accounts/show.html.haml",
-      "line": 125,
+      "line": 146,
       "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
       "code": "link_to(Account.find(params[:id]).outbox_url, Account.find(params[:id]).outbox_url)",
-      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+      "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
       "location": {
         "type": "template",
         "template": "admin/accounts/show"
@@ -269,6 +288,6 @@
       "note": ""
     }
   ],
-  "updated": "2017-10-20 00:00:54 +0900",
+  "updated": "2017-11-19 20:34:18 +0100",
   "brakeman_version": "4.0.1"
 }
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 08a96f727..014055804 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -60,3 +60,4 @@ ignore_unused:
   - 'activerecord.errors.models.doorkeeper/*'
   - 'errors.429'
   - 'admin.accounts.roles.*'
+  - 'admin.action_logs.actions.*'
diff --git a/config/locales/activerecord.ca.yml b/config/locales/activerecord.ca.yml
index 12e347ad9..39f6839aa 100644
--- a/config/locales/activerecord.ca.yml
+++ b/config/locales/activerecord.ca.yml
@@ -10,4 +10,4 @@ ca:
         status:
           attributes:
             reblog:
-              taken: del estat ja existeix
+              taken: de l'estat ja existeix
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 1dc6c492b..fa8cf49f0 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -1,12 +1,13 @@
 ---
 ca:
   about:
+    about_hashtag_html: Aquests són toots públics etiquetats amb <strong>#%{hashtag}</strong>. Pots interactuar amb ells si tens un compte a qualsevol lloc del fediverse.
     about_mastodon_html: Mastodon és un servidor de xarxa social <em>lliure i de codi obert</em>. Una alternativa <em>descentralitzada</em> a plataformes comercials, que evita el risc que una única companyia monopolitzi la teva comunicació. Qualsevol pot executar Mastodon i participar sense problemes en la <em>xarxa social</em>.
-    about_this: Sobre aquesta instància
+    about_this: Quant a aquesta instància
     closed_registrations: Els registres estan actualment tancats en aquesta instància.
     contact: Contacte
     contact_missing: No configurat
-    contact_unavailable: N/A
+    contact_unavailable: N/D
     description_headline: Què es %{domain}?
     domain_count_after: altres instàncies
     domain_count_before: Connectat a
@@ -14,61 +15,85 @@ ca:
       <h3>Un bon lloc per les regles</h3>
       <p>Encara no s'ha configurat la descripció ampliada.</p>
     features:
-      humane_approach_body: Aprenent dels errors d'altres xarxes, Mastodon té com a objectiu fer ètiques eleccions de disseny per combatre el mal ús de les xarxes socials.
+      humane_approach_body: Aprenent dels errors d'altres xarxes, Mastodon té com a objectiu fer eleccions ètiques de disseny per a combatre el mal ús de les xarxes socials.
       humane_approach_title: Un enfocament més humà
-      not_a_product_body: Mastodon no és una xarxa comercial. Sense publicitat, sense mineria de dades, sense jardins amurallats. No hi ha autoritat central.
+      not_a_product_body: Mastodon no és una xarxa comercial. Sense publicitat, sense mineria de dades, sense jardins emmurallats. No hi ha autoritat central.
       not_a_product_title: Ets una persona, no un producte
       real_conversation_body: Amb 500 caràcters a la teva disposició i suport per a continguts granulars i avisos multimèdia, pots expressar-te de la manera que vulguis.
       real_conversation_title: Construït per a converses reals
-      within_reach_body: Diverses aplicacions per a iOS, Android i altres plataformes gràcies a un ecosistema API amable amb el desenvolupador, et permet mantenir-te al dia amb els teus amics en qualsevol lloc..
+      within_reach_body: Diverses aplicacions per a iOS, Android i altres plataformes gràcies a un ecosistema API amable amb el desenvolupador, et permet mantenir-te al dia amb els amics en qualsevol lloc..
       within_reach_title: Sempre a l'abast
     find_another_instance: Troba altres instàncies
     generic_description: "%{domain} és un servidor a la xarxa"
     hosted_on: Mastodon allotjat a %{domain}
-    learn_more: Aprèn més
+    learn_more: Més informació
     other_instances: Altres instàncies
     source_code: Codi font
     status_count_after: estats
-    status_count_before: Que han escrit
+    status_count_before: que han escrit
     user_count_after: usuaris registrats
     user_count_before: Tenim
     what_is_mastodon: Què és Mastodon?
   accounts:
-    follow: Seguir
+    follow: Segueix
     followers: Seguidors
     following: Seguint
+    media: Mèdia
+    moved_html: "%{name} s'ha mogut a %{new_profile_link}:"
     nothing_here: No hi ha res aquí!
     people_followed_by: Usuaris a qui %{name} segueix
-    people_who_follow: Usuaris que segueixn a %{name}
+    people_who_follow: Usuaris que segueixen %{name}
     posts: Toots
-    remote_follow: Seguir
+    posts_with_replies: Toots i respostes
+    remote_follow: Seguiment remot
     reserved_username: El nom d'usuari està reservat
+    roles:
+      admin: Admin
+      moderator: Mod
     unfollow: Deixar de seguir
   admin:
+    account_moderation_notes:
+      account: Moderador
+      create: Crear
+      created_at: Data
+      created_msg: La nota de moderació s'ha creat correctament.
+      delete: Suprimeix
+      destroyed_msg: S'ha destruït la nota de moderació.
     accounts:
       are_you_sure: Estàs segur?
+      by_domain: Domini
       confirm: Confirma
       confirmed: Confirmat
+      demote: Degrada
+      disable: Inhabilita
       disable_two_factor_authentication: Desactivar 2FA
+      disabled: Inhabilita
       display_name: Nom de visualització
       domain: Domini
       edit: Editar
       email: E-mail
+      enable: Habilitar
+      enabled: Habilitat
       feed_url: URL del feed
       followers: Seguidors
+      followers_url: URL dels seguidors
       follows: Segueix
+      inbox_url: URL de la safata d'entrada
       ip: IP
       location:
         all: Tot
         local: Local
         remote: Remot
         title: Localització
+      login_status: Estat d'accés
       media_attachments: Adjunts multimèdia
+      memorialize: Es converteix en memoriam
       moderation:
         all: Tot
         silenced: Silenciat
         suspended: Suspès
         title: Moderació
+      moderation_notes: Notes de moderació
       most_recent_activity: Activitat més recent
       most_recent_ip: IP més recent
       not_subscribed: No subscrit
@@ -76,64 +101,109 @@ ca:
         alphabetic: Alfabètic
         most_recent: Més recent
         title: Ordre
+      outbox_url: URL de la bústia de sortida
       perform_full_suspension: Aplicar suspensió completa
       profile_url: URL del perfil
+      promote: Promociona
+      protocol: Protocol
       public: Públic
       push_subscription_expires: La subscripció PuSH expira
       redownload: Refrescar avatar
       reset: Reajustar
       reset_password: Restablir la contrasenya
       resubscribe: Resubscribir
+      role: Permisos
+      roles:
+        admin: Administrador
+        moderator: Moderador
+        user: Usuari
       salmon_url: URL Salmon
       search: Cerca
+      shared_inbox_url: URL de la safata d'entrada compartida
       show:
         created_reports: Informes creats per aquest compte
         report: informe
         targeted_reports: Informes realitzats sobre aquest compte
       silence: Silenci
       statuses: Estats
-      subscribe: Subscribir
+      subscribe: Subscriu
       title: Comptes
-      undo_silenced: Desfer silenci
-      undo_suspension: Desfer suspensió
+      undo_silenced: Deixa de silenciar
+      undo_suspension: Desfés la suspensió
       unsubscribe: Donar-se de baixa
       username: Nom d'usuari
       web: Web
+    custom_emojis:
+      copied_msg: S'ha creat correctament la còpia local del emoji
+      copy: Copia
+      copy_failed_msg: No s'ha pogut fer una còpia local d'aquest emoji
+      created_msg: Emoji creat amb èxit!
+      delete: Suprimeix
+      destroyed_msg: Emojo s'ha destruït amb èxit!
+      disable: Inhabilita
+      disabled_msg: S'ha inhabilitat l'emoji amb èxit
+      emoji: Emoji
+      enable: Habilita
+      enabled_msg: S'ha habilitat amb èxit emoji
+      image_hint: PNG de fins a 50 KB
+      listed: Enumerat
+      new:
+        title: Afegeix nou emoji personalitzat
+      overwrite: Sobreescriure
+      shortcode: Codi curt
+      shortcode_hint: Com a mínim 2 caràcters, només caràcters alfanumèrics i guions baixos
+      title: Emojis personatlitzats
+      unlisted: Sense classificar
+      update_failed_msg: No s'ha pogut actualitzar aquest emoji
+      updated_msg: Emoji s'ha actualitzat correctament.
+      upload: Carrega
     domain_blocks:
-      add_new: Afegir nou
+      add_new: Afegeix
       created_msg: El bloqueig de domini ara s'està processant
       destroyed_msg: El bloqueig de domini s'ha desfet
       domain: Domini
       new:
-        create: Crear bloqueig
+        create: Crea un bloqueig
         hint: El bloqueig de domini no impedirà la creació de nous comptes en la base de dades, però s´aplicaran mètodes de moderació específics sobre aquests comptes
         severity:
-          desc_html: "<strong>Silenci</strong> farà les publicacions del compte invisibles a tothom que no l'estigui seguint. Suspendre eliminarà tots els continguts, multimèdia i les dades del perfil del compte."
+          desc_html: "<strong>Silenci</strong> farà les publicacions del compte invisibles a tothom que no l'estigui seguint. La suspencsió eliminarà tots els continguts, multimèdia i les dades del perfil del compte."
           noop: Cap
           silence: Silenci
-          suspend: Suspendre
+          suspend: Suspensió
         title: Nou bloqueig de domini
-      reject_media: Rebutjar arxius multimèdia
-      reject_media_hint: Elimina arxius multimèdia emmagatzamats localment i impideix descarregar cap en el futur. Irrellevant per suspensions
+      reject_media: Rebutja els fitxers multimèdia
+      reject_media_hint: Elimina els fitxers multimèdia emmagatzamats localment i impideix descarregar-ne cap en el futur. Irrellevant en les suspensions
       severities:
         noop: Cap
         silence: Silenci
-        suspend: Suspendre
+        suspend: Suspensió
       severity: Severitat
       show:
         affected_accounts:
           one: Un compte afectat en la base de dades
           other: "%{count} comptes afectats en la base de dades"
         retroactive:
-          silence: Desfer el silenci a tots els comptes existents d'aquest domini
-          suspend: Desfer la suspensió de tots els comptes d'aquest domini
-        title: Desfer el bloqueig de domini de %{domain}
-        undo: Desfer
+          silence: Desfés el silenci a tots els comptes existents d'aquest domini
+          suspend: Desfés la suspensió de tots els comptes d'aquest domini
+        title: Desfés el bloqueig de domini de %{domain}
+        undo: Desfés
       title: Bloquejos de domini
       undo: Desfer
+    email_domain_blocks:
+      add_new: Afegir nou
+      created_msg: S'ha creat el bloc de domini de correu electrònic
+      delete: Suprimeix
+      destroyed_msg: S'ha eliminat correctament el bloc del domini de correu
+      domain: Domini
+      new:
+        create: Crear bloc
+        title: Nou bloc de domini de correu electrònic
+      title: Bloc de domini de correu electrònic
     instances:
       account_count: Comptes coneguts
       domain_name: Domini
+      reset: Restablir
+      search: Cerca
       title: Instàncies conegudes
     reports:
       action_taken_by: Mesures adoptades per
@@ -141,48 +211,57 @@ ca:
       comment:
         label: Comentari
         none: Cap
-      delete: Esborra
+      delete: Suprimeix
       id: ID
       mark_as_resolved: Marca com a resolt
       nsfw:
-        'false': NSFW OFF
-        'true': NSFW ON
+        'false': Mostra els fitxers multimèdia adjunts
+        'true': Amaga els fitxers multimèdia adjunts
       report: 'Informe #%{id}'
       report_contents: Continguts
       reported_account: Compte reportat
       reported_by: Reportat per
       resolved: Resolt
-      silence_account: Silenciar compte
+      silence_account: Silencia el compte
       status: Estat
-      suspend_account: Suspendre compte
+      suspend_account: Suspèn el compte
       target: Objectiu
       title: Informes
       unresolved: No resolt
-      view: Vista
+      view: Visualització
     settings:
+      bootstrap_timeline_accounts:
+        desc_html: Separa diversos noms d'usuari amb comes. Només funcionaran els comptes locals i desbloquejats. El valor predeterminat quan està buit és tots els administradors locals..
+        title: El seguiment per defecte per als nous usuaris
       contact_information:
-        email: Introduir una adreça de correu electrònic pùblica
-        username: Introduir un nom d'usuari
+        email: Introdueix una adreça de correu electrònic pùblica
+        username: Introdueix un nom d'usuari
       registrations:
         closed_message:
-          desc_html: Apareix en la primera pàgina quan es tanquen els registres<br>Pot utilitzar etiquetes HTML
+          desc_html: Apareix en la primera pàgina quan es tanquen els registres<br>Pots utilitzar etiquetes HTML
           title: Missatge de registre tancat
         deletion:
           desc_html: Permet a qualsevol esborrar el seu compte
-          title: Obrir la supressió del compte
+          title: Obre la supressió del compte
         open:
           desc_html: Permet que qualsevol pugui crear un compte
           title: Registre obert
+      show_staff_badge:
+        desc_html: Mostra una insígnia de personal en una pàgina d'usuari
+        title: Mostra insígnia de personal
       site_description:
         desc_html: Es mostra com un paràgraf a la pàgina principal i s'utilitza com una etiqueta meta.<br>Pots utilitzar etiquetes HTML, en particular <code>&lt;a&gt;</code> i <code>&lt;em&gt;</code>.
         title: Descripció del lloc
       site_description_extended:
-        desc_html: Un bon lloc per al vostre codi de conducta, regles, directrius i altres coses que distingeixen la vostra instància. Podeu utilitzar etiquetes HTML
-        title: Descripció estesa del lloc
+        desc_html: Un bon lloc per al codi de conducta, regles, directrius i altres coses que distingeixen la vostra instància. Pots utilitzar etiquetes HTML
+        title: Descripció ampliada del lloc
       site_terms:
         desc_html: Pots escriure la teva pròpia política de privadesa, els termes del servei o d'altres normes legals. Pots utilitzar etiquetes HTML
         title: Termes del servei personalitzats
       site_title: Títol del lloc
+      thumbnail:
+        desc_html: S'utilitza per obtenir visualitzacions prèvies a través d'OpenGraph i API. Es recomana 1200x630px
+        title: Miniatura de la Instància
       timeline_preview:
         desc_html: Mostra la línia de temps pública a la pàgina inicial
         title: Vista prèvia de la línia de temps
@@ -190,18 +269,18 @@ ca:
     statuses:
       back_to_account: Torna a la pàgina del compte
       batch:
-        delete: Esborra
+        delete: Suprimeix
         nsfw_off: NSFW OFF
         nsfw_on: NSFW ON
       execute: Executa
       failed_to_execute: No s'ha pogut executar
       media:
-        hide: Amaga multimèdia
-        show: Mostra multimèdia
-        title: Multimèdia
-      no_media: Sense multimèdia
+        hide: Amaga el contingut multimèdia
+        show: Mostra el contingut multimèdia
+        title: Contingut multimèdia
+      no_media: Sense contingut multimèdia
       title: Estats del compte
-      with_media: Amb multimèdia
+      with_media: Amb contingut multimèdia
     subscriptions:
       callback_url: Callback URL
       confirmed: Confirmat
@@ -216,127 +295,137 @@ ca:
       subject: Nou informe per a %{instance} (#%{id})
   application_mailer:
     salutation: "%{name},"
-    settings: 'Canviar preferències de correu: %{link}'
-    signature: Notificacions de Mastodon desde %{instance}
+    settings: 'Canvia les preferències de correu: %{link}'
+    signature: Notificacions de Mastodon des de %{instance}
     view: 'Vista:'
   applications:
+    created: L'aplicació s'ha creat correctament
+    destroyed: L'aplicació s'ha suprimit correctament
     invalid_url: La URL proporcionada es incorrecte
+    regenerate_token: Regenerar token d'accés
+    token_regenerated: Token d'accés s'ha generat correctament
+    warning: Aneu amb compte amb aquestes dades. No ho compartiu mai amb ningú!
+    your_token: El token d'accés
   auth:
-    agreement_html: En inscriure't, acceptes <a href="%{rules_path}">les nostres termes del servei</a> i <a href="%{terms_path}">la nostra política de privadesa</a>.
-    change_password: Canviar contrasenya
-    delete_account: Esborrar el compte
-    delete_account_html: Si vols esborrar el teu compte pots <a href="%{path}">fer-ho aquí</a>. S'et demanarà confirmació.
-    didnt_get_confirmation: No vas rebre el correu de confirmació?
+    agreement_html: En inscriure't, acceptes <a href="%{rules_path}">els nostres termes del servei</a> i <a href="%{terms_path}">la nostra política de privadesa</a>.
+    change_password: Canvia la contrasenya
+    delete_account: Esborra el compte
+    delete_account_html: Si vols esborrar el teu compte pots <a href="%{path}">fer-ho aquí</a>. Se't demanarà confirmació.
+    didnt_get_confirmation: No ha rebut el correu de confirmació?
     forgot_password: Has oblidat la contrasenya?
-    invalid_reset_password_token: L'enllaç de restabliment de la contrasenya no és vàlid o caducat. Siusplau torna-ho a provar..
-    login: Iniciar sessió
-    logout: Tancar sessió
-    register: Enregistrarse
-    resend_confirmation: Tornar a enviar el correu de confirmació
-    reset_password: Restablir contrasenya
-    set_new_password: Establir nova contrasenya
+    invalid_reset_password_token: L'enllaç de restabliment de la contrasenya no és vàlid o ha caducat. Torna-ho a provar..
+    login: Inicia sessió
+    logout: Tanca sessió
+    register: Registra't
+    resend_confirmation: Torna a enviar el correu de confirmació
+    reset_password: Restableix la contrasenya
+    set_new_password: Estableix nova contrasenya
   authorize_follow:
     error: Malauradament, ha ocorregut un error buscant el compte remot
-    follow: Seguir
+    follow: Segueix
     follow_request: 'Heu enviat una sol·licitud de seguiment a:'
-    following: 'Èxit! Ara segueixes:'
+    following: 'Perfecte! Ara segueixes:'
     post_follow:
       close: O bé, pots tancar aquesta finestra.
       return: Torna al perfil de l'usuari
-      web: Anar a la web
-    title: Seguir %{acct}
+      web: Vés a la web
+    title: Segueix %{acct}
   datetime:
     distance_in_words:
-      about_x_hours: "%{count}h"
-      about_x_months: "%{count}m"
-      about_x_years: "%{count}y"
-      almost_x_years: "%{count}y"
+      about_x_hours: "%{count} h"
+      about_x_months: "%{count} mesos"
+      about_x_years: "%{count} anys"
+      almost_x_years: "%{count}anys"
       half_a_minute: Ara mateix
       less_than_x_minutes: "%{count}m"
       less_than_x_seconds: Ara mateix
-      over_x_years: "%{count}y"
-      x_days: "%{count}d"
-      x_minutes: "%{count}m"
-      x_months: "%{count}m"
-      x_seconds: "%{count}s"
+      over_x_years: "%{count} anys"
+      x_days: "%{count} dies"
+      x_minutes: "%{count} min"
+      x_months: "%{count} mesos"
+      x_seconds: "%{count} s"
   deletes:
-    bad_password_msg: Bon intent hackers! Contrasenya incorrecta
-    confirm_password: Introdueix la contrasenya actual per verificar la teva identitat
+    bad_password_msg: Bon intent hackers! La contrasenya no és correcta
+    confirm_password: Introdueix la contrasenya actual per a verificar la teva identitat
     description_html: Això eliminarà de forma <strong>irreversible i permanent</strong> el contingut del teu compte i el desactivarà. El teu nom d'usuari romandrà reservat per evitar que algú volgués fer-se passar per tu.
-    proceed: Esborrar el compte
+    proceed: Suprimir el compte
     success_msg: El teu compte s'ha eliminat correctament
     warning_html: Només està garantida l'eliminació d'aquesta particular instància. El contingut que ha estat àmpliament compartit que deixi petjades. Els servidors fora de línia i els que ja no estan subscrits no actualitzaran les seves bases de dades.
     warning_title: Disponibilitat de contingut disseminat
   errors:
-    '403': No tens permís per veure aquesta pàgina.
-    '404': La pàgina que estàs buscant no existeix.
-    '410': La pàgina que estaves buscant ja no existeix.
+    '403': No tens permís per a veure aquesta pàgina.
+    '404': La pàgina que estàs cercant no existeix.
+    '410': La pàgina que estàs cercant ja no existeix.
     '422':
-      content: La verificació de seguretat ha fallat. Bloquejes les galetes?
+      content: La verificació de seguretat ha fallat. Bloques les galetes?
       title: La verificació de seguretat ha fallat
     '429': Estrangulat
-    noscript_html: Per utilitzar Mastodon si us plau activa JavaScript.
+    '500':
+      content: We're sorry, but something went wrong on our end.
+      title: This page is not correct
+    noscript_html: Per utilitzar Mastodon si us plau activa JavaScript. També podeu provar una de les <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md"> aplicacions natives</a> per Mastodon per a la vostra plataforma.
   exports:
     blocks: Persones que has bloquejat
     csv: CSV
     follows: Persones que segueixes
-    mutes: Persones apagades (muted)
+    mutes: Persones silenciades
     storage: Emmagatzematge
   followers:
     domain: Domini
-    explanation_html: Si desitges garantir la privacitat de les teves publicacions, has de ser conscient de qui t'està seguint. <strong> Les teves publicacions privades es lliuren a totes les instàncies on es té seguidors </ strong>. És possible que vulguis revisar-los i eliminar seguidors si no confies en que la teva privacitat sigui respectada pel personal o el programari d'aquests casos
+    explanation_html: Si desitges garantir la privacitat de les teves publicacions, has de ser conscient de qui t'està seguint. <strong> Les publicacions privades es lliuren a totes les instàncies on tens seguidors </ strong>. És possible que vulguis revisar-los i eliminar seguidors si no confies en que la teva privacitat sigui respectada pel personal o el programari d'aquests casos
     followers_count: Nombre de seguidors
-    lock_link: Bloqueja el teu compte
-    purge: Eliminar de seguidors
+    lock_link: Bloca el teu compte
+    purge: Elimina dels seguidors
     success:
       one: En el procés de bloqueig suau de seguidors d'un domini...
       other: En el procés de bloqueig suau de seguidors de %{count} dominis...
-    true_privacy_html: Si us plau considera que <strong>la autèntica privacitat només es pot aconseguir amb xifrat d'extrem a extrem</strong>.
-    unlocked_warning_html: Tothom pot seguir-te per veure inmediatament les teves publicacions privades. %{lock_link} per poder revisar i rebutjar seguidors.
-    unlocked_warning_title: El teu compte no està bloquejat
+    true_privacy_html: Considera que <strong>la autèntica privacitat només es pot aconseguir amb xifratge d'extrem a extrem</strong>.
+    unlocked_warning_html: Tothom pot seguir-te per a veure inmediatament les teves publicacions privades. %{lock_link} per poder revisar i rebutjar seguidors.
+    unlocked_warning_title: El teu compte no està blocat
   generic:
-    changes_saved_msg: Canvis guardats amb èxit!
-    powered_by: powered by %{link}
-    save_changes: Guardar canvis
+    changes_saved_msg: Els canvis s'han desat correctament!
+    powered_by: amb tecnologia %{link}
+    save_changes: Desa els canvis
     validation_errors:
-      one: Alguna cosa no esta bé! Si us plau, revisi l'error
-      other: Alguna cosa no esta bé! Si us plau, revisi %{count} errors més a baix
+      one: Alguna cosa no va bé! Si us plau, revisa l'error
+      other: Alguna cosa no va bé! Si us plau, revisa %{count} errors més a baix
   imports:
-    preface: Pots importar algunes dades, com ara totes les persones que estàs seguint o bloquejant, en el teu compte en aquesta instància, desde arxius exportats desde una altra instància.
-    success: Dades rebudes correctament i seran processades en breu
+    preface: Pots importar algunes dades, com ara totes les persones que estàs seguint o blocant, en el teu compte en aquesta instància, des de fitxers exportats en una altra instància.
+    success: Les dades s'han rebut correctament i es processaran en breu
     types:
-      blocking: Llista de bloqueajats
+      blocking: Llista de blocats
       following: Llista de seguits
       muting: Llista d'apagats
     upload: Carregar
+  in_memoriam_html: En Memòria.
   landing_strip_html: "<strong>%{name}</strong> és un usuari/a de %{link_to_root_path}. Pots seguir-lo/la o interactuar amb ell/a si tens un compte a qualsevol node del fediverse."
   landing_strip_signup_html: Si no en tens, pots <a href="%{sign_up_path}">registrar-te aquí</a>.
   media_attachments:
     validations:
       images_and_video: No es pot adjuntar un vídeo a una publicació que ja contingui imatges
-      too_many: No es poden adjuntar més de 4 arxius
+      too_many: No es poden adjuntar més de 4 fitxers
   notification_mailer:
     digest:
-      body: 'Un resum del que et vas perdre en %{instance} desde la teva darrera visita el %{since}:'
+      body: 'Un resum del que et vas perdre en %{instance} desde la darrera visita el %{since}:'
       mention: "%{name} t'ha mencionat en:"
       new_followers_summary:
         one: Visca!. Algú més t´ha començat a seguir
-        other: Genial!. T'han seguit %{count} noves persones
+        other: Genial!. Et segueixen %{count} persones més
       subject:
-        one: "1 nova notificació desde la teva darrera visita \U0001F418"
-        other: "%{count} noves notificacions desde la teva darrera visita \U0001F418"
+        one: "1 notificació nova des de la darrera visita \U0001F418"
+        other: "%{count} notificacions noves des de la darrera visita \U0001F418"
     favourite:
-      body: 'El teu estat ha estat marcat com a favorit per %{name}:'
+      body: "%{name} ha marcat com a favorit el teu estat:"
       subject: "%{name} ha marcat com a favorit el teu estat"
     follow:
-      body: "¡%{name} t'està seguint!"
+      body: "%{name} t'està seguint!"
       subject: "%{name} t'està seguint"
     follow_request:
       body: "%{name} ha sol·licitat seguir-te"
       subject: 'Seguidor pendent: %{name}'
     mention:
-      body: 'Has estat mencionat per %{name} en:'
-      subject: Has estat mencionat per %{name}
+      body: "%{name} t'ha mencionat en:"
+      subject: "%{name} t'ha mencionat"
     reblog:
       body: "%{name} ha retootejat el teu estat"
       subject: "%{name} ha retootejat el teu estat"
@@ -348,31 +437,36 @@ ca:
           billion: B
           million: M
           quadrillion: Q
-          thousand: K
+          thousand: m
           trillion: T
           unit: ''
   pagination:
-    next: Pròxim
-    prev: Anterior
+    next: Següent
+    prev: Enrere
     truncate: "&hellip;"
+  preferences:
+    languages: Idiomes
+    other: Altre
+    publishing: Publicació
+    web: Web
   push_notifications:
     favourite:
-      title: "%{name} ha afavorit el teu estat"
+      title: "%{name} ha marcat com a preferit el teu estat"
     follow:
       title: "%{name} ara et segueix"
     group:
       title: "%{count} notificacions"
     mention:
-      action_boost: Boost
-      action_expand: Mostra més
-      action_favourite: Favorit
+      action_boost: Retooteja
+      action_expand: Mostra'n més
+      action_favourite: Preferit
       title: "%{name} t'ha mencionat"
     reblog:
       title: "%{name} t'ha retootejat"
   remote_follow:
-    acct: Escriu el usuari@domini de la persona que vols seguir
-    missing_resource: No s'ha pogut trobar la URL de redirecció necessaria per el compte.
-    proceed: Procedir a seguir
+    acct: Escriu l'usuari@domini de la persona que vols seguir
+    missing_resource: No s'ha pogut trobar la URL de redirecció necessaria per al compte.
+    proceed: Comença a seguir
     prompt: 'Seguiràs a:'
   sessions:
     activity: Última activitat
@@ -395,7 +489,7 @@ ca:
       weibo: Weibo
     current_session: Sessió actual
     description: "%{browser} de %{platform}"
-    explanation: Aquests són els navegadors web que actualment han iniciat la sessió al teu compte de Mastodon.
+    explanation: Aquests són els navegadors web que actualment han iniciat la sessió amb el teu compte de Mastodon.
     ip: IP
     platforms:
       adobe_air: Adobe Air
@@ -415,53 +509,129 @@ ca:
     title: Sessions
   settings:
     authorized_apps: Aplicacions autoritzades
-    back: Tornar al inici
+    back: Torna a l'inici
     delete: Eliminació del compte
+    development: Desenvolupament
     edit_profile: Editar perfil
     export: Exportar informació
     followers: Seguidors autoritzats
     import: Importar
+    notifications: Notificacions
     preferences: Preferències
     settings: Configuració
     two_factor_authentication: Autenticació de dos factors
+    your_apps: Les teves aplicacions
   statuses:
-    open_in_web: Obrir en la web
+    open_in_web: Obre en la web
     over_character_limit: Límit de caràcters de %{max} superat
+    pin_errors:
+      limit: S'han fixat massa toots
+      ownership: El toot d'algú altre no es pot fixar
+      private: No es pot fixar el toot no públic
+      reblog: No es pot fixar un impuls
     show_more: Mostrar més
     visibilities:
       private: Només seguidors
-      private_long: Només mostrar a seguidors
+      private_long: Mostra només als seguidors
       public: Públic
       public_long: Tothom pot veure-ho
       unlisted: No llistat
       unlisted_long: Tothom ho pot veure, però no es mostra en la història federada
   stream_entries:
     click_to_show: Clic per mostrar
-    reblogged: retooteado
+    pinned: Toot fixat
+    reblogged: impulsat
     sensitive_content: Contingut sensible
   terms:
-    body_html: "<h2>Política de privacitat</h2>\n\n<h3 id=\"collect\">Quina informació recollim?</h3>\n\n<p>Recopilem informació teva quan et registres en aquesta instància i recopilem dades quan participes en el fòrum llegint, escrivint i avaluant el contingut aquí compartit.</p>\n\n<p>En registrar-te en aquesta instància, se't pot demanar que introduexisu el teu nom i l'adreça de correu electrònic. També pots visitar el nostre lloc sense registrar-te. La teva adreça de correu electrònic es verificarà mitjançant un correu electrònic que conté un enllaç únic. Si es visita aquest enllaç, sabem que controles l'adreça de correu electrònic.</p>\n\n<p>Quan es registra i publica, registrem l'adreça IP de la qual es va originar la publicació. També podrem conservar els registres del servidor que inclouen l'adreça IP de cada sol·licitud al nostre servidor.</p>\n\n<h3 id=\"use\">Per a què utilitzem la teva informació?</h3>\n\n<p>Qualsevol de la informació que recopilem de tu pot utilitzar-se d'una de les maneres següents:</p>\n\n<ul>\n  <li>Per a personalitzar la teva experiència &mdash; la teva informació ens ajuda a respondre millor a les teves necessitats individuals.</li>\n  <li>Per millorar el nostre lloc &mdash; ens esforcem contínuament per millorar les nostres ofertes de llocs basats en la informació i els comentaris que rebem de tu.</li>\n  <li>Per millorar el servei al client &mdash; la teva informació ens ajuda a respondre més eficaçment a les teves sol·licituds de servei al client i a les necessitats de suport.</li>\n  <li>Per enviar correus electrònics periòdics &mdash; l'adreça electrònica que proporcionis es pot utilitzar per enviar-te informació, notificacions que sol·licitis sobre canvis en temes o en resposta al teu nom d'usuari, respondre a les consultes i/o altres sol·licituds o preguntes.</li>\n</ul>\n\n<h3 id=\"protect\">Com protegim la teva informació?</h3>\n\n<p>Implementem diverses mesures de seguretat per mantenir la seguretat de la teva informació personal quan introdueixes, envies o accedeixes a la teva informació personal.</p>\n\n<h3 id=\"data-retention\">Quina és la nostre política de retenció de dades?</h3>\n\n<p>Farem un esforç de bona fe per a:</p>\n\n<ul>\n  <li>Conserva els registres de servidor que continguin l'adreça IP de totes les sol·licituds a aquest servidor no més de 90 dies.</li>\n  <li>Conserva les adreces IP associades als usuaris registrats i les seves publicacions no més de 5 anys.</li>\n</ul>\n\n<h3 id=\"cookies\">Utilitzem galetes?</h3>\n\n<p>Sí. Les cookies són fitxers petits que un lloc o el proveïdor de serveis transfereix al disc dur del vostre ordinador a través del navegador web (si ho permet). Aquestes galetes permeten al lloc reconèixer el vostre navegador i, si teniu un compte registrat, associar-lo al vostre compte registrat.</p>\n\n<p>Utilitzem cookies per comprendre i desar les vostres preferències per a futures visites i compilar dades agregades sobre el trànsit del lloc i la interacció del lloc, de manera que podrem oferir millors experiències i eines del lloc en el futur. Podem contractar amb proveïdors de serveis de tercers per ajudar-nos a comprendre millor els visitants del nostre lloc. Aquests proveïdors de serveis no estan autoritzats a utilitzar la informació recollida en nom nostre, excepte per ajudar-nos a dur a terme i millorar el nostre negoci.</p>\n\n<h3 id=\"disclose\">Publiquem informació al exterior?</h3>\n\n<p>No venem, comercialitzem ni transmetem a tercers la vostra informació d'identificació personal. Això no inclou tercers de confiança que ens ajudin a operar el nostre lloc, a dur a terme el nostre negoci o a fer-ho, sempre que aquestes parts acceptin mantenir confidencial aquesta informació. També podem publicar la vostra informació quan creiem que l'alliberament és apropiat per complir amb la llei, fer complir les polítiques del nostre lloc o protegir els nostres drets o altres drets, propietat o seguretat. No obstant això, la informació de visitant que no sigui personalment identificable es pot proporcionar a altres parts per a la comercialització, la publicitat o altres usos.</p>  \n\n<h3 id=\"third-party\">Vincles de tercers</h3>\n\n<p>De tant en tant, segons el nostre criteri, podem incloure o oferir productes o serveis de tercers al nostre lloc. Aquests llocs de tercers tenen polítiques de privadesa separades i independents. Per tant, no tenim responsabilitat ni responsabilitat civil pel contingut i les activitats d'aquests llocs enllaçats. No obstant això, busquem protegir la integritat del nostre lloc i donem la benvinguda a qualsevol comentari sobre aquests llocs.</p>\n\n<h3 id=\"coppa\">Compliment de la Llei de protecció de la privacitat en línia dels nens</h3>\n\n<p>El nostre lloc, productes i serveis estan dirigits a persones que tenen almenys 13 anys. Si aquest servidor es troba als EUA, i teniu menys de 13 anys, segons els requisits de COPPA (<a href=\"https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act\">Children's Online Privacy Protection Act</a>) no feu servir aquest lloc.</p>\n\n<h3 id=\"online\">Només la política de privacitat en línia</h3>\n\n<p>Aquesta política de privacitat en línia només s'aplica a la informació recopilada a través del nostre lloc i no a la informació recopilada fora de línia.</p>\n\n<h3 id=\"consent\">El vostre consentiment</h3>\n\n<p>En utilitzar el nostre lloc, accepta la política de privadesa del nostre lloc web.</p>\n\n<h3 id=\"changes\">Canvis a la nostra política de privacitat</h3>\n\n<p>Si decidim canviar la nostra política de privadesa, publicarem aquests canvis en aquesta pàgina.</p>\n\n<p>Aquest document és CC-BY-SA. Es va actualitzar per última vegada el 31 de maig de 2013.</p>\n\n<p>Originalment adaptat a la <a href=\"https://github.com/discourse/discourse\">política de privadesa del Discurs</a>.</p>\n"
+    body_html: |
+      <h2>Política de privacitat</h2>
+
+      <h3 id="collect">Quina informació recollim?</h3>
+
+      <p>Recopilem informació teva quan et registres en aquesta instància i recopilem dades quan participes en el fòrum llegint, escrivint i avaluant el contingut aquí compartit.</p>
+
+      <p>En registrar-te en aquesta instància, se't pot demanar que introduexisu el teu nom i l'adreça de correu electrònic. També pots visitar el nostre lloc sense registrar-te. La teva adreça de correu electrònic es verificarà mitjançant un correu electrònic que conté un enllaç únic. Si es visita aquest enllaç, sabem que controles l'adreça de correu electrònic.</p>
+
+      <p>Quan es registra i publica, registrem l'adreça IP de la qual es va originar la publicació. També podrem conservar els registres del servidor que inclouen l'adreça IP de cada sol·licitud al nostre servidor.</p>
+
+      <h3 id="use">Per a què utilitzem la teva informació?</h3>
+
+      <p>Qualsevol de la informació que recopilem de tu pot utilitzar-se d'una de les maneres següents:</p>
+
+      <ul>
+        <li>Per a personalitzar la teva experiència &mdash; la teva informació ens ajuda a respondre millor a les teves necessitats individuals.</li>
+        <li>Per millorar el nostre lloc &mdash; ens esforcem contínuament per millorar les nostres ofertes de llocs basats en la informació i els comentaris que rebem de tu.</li>
+        <li>Per millorar el servei al client &mdash; la teva informació ens ajuda a respondre més eficaçment a les teves sol·licituds de servei al client i a les necessitats de suport.</li>
+        <li>Per enviar correus electrònics periòdics &mdash; l'adreça electrònica que proporcionis es pot utilitzar per enviar-te informació, notificacions que sol·licitis sobre canvis en temes o en resposta al teu nom d'usuari, respondre a les consultes i/o altres sol·licituds o preguntes.</li>
+      </ul>
+
+      <h3 id="protect">Com protegim la teva informació?</h3>
+
+      <p>Implementem diverses mesures de seguretat per mantenir la seguretat de la teva informació personal quan introdueixes, envies o accedeixes a la teva informació personal.</p>
+
+      <h3 id="data-retention">Quina és la nostre política de retenció de dades?</h3>
+
+      <p>Farem un esforç de bona fe per a:</p>
+
+      <ul>
+        <li>Conserva els registres de servidor que continguin l'adreça IP de totes les sol·licituds a aquest servidor no més de 90 dies.</li>
+        <li>Conserva les adreces IP associades als usuaris registrats i les seves publicacions no més de 5 anys.</li>
+      </ul>
+
+      <h3 id="cookies">Utilitzem galetes?</h3>
+
+      <p>Sí. Les cookies són fitxers petits que un lloc o el proveïdor de serveis transfereix al disc dur del vostre ordinador a través del navegador web (si ho permet). Aquestes galetes permeten al lloc reconèixer el vostre navegador i, si teniu un compte registrat, associar-lo al vostre compte registrat.</p>
+
+      <p>Utilitzem cookies per comprendre i desar les vostres preferències per a futures visites i compilar dades agregades sobre el trànsit del lloc i la interacció del lloc, de manera que podrem oferir millors experiències i eines del lloc en el futur. Podem contractar amb proveïdors de serveis de tercers per ajudar-nos a comprendre millor els visitants del nostre lloc. Aquests proveïdors de serveis no estan autoritzats a utilitzar la informació recollida en nom nostre, excepte per ajudar-nos a dur a terme i millorar el nostre negoci.</p>
+
+      <h3 id="disclose">Publiquem informació al exterior?</h3>
+
+      <p>No venem, comercialitzem ni transmetem a tercers la vostra informació d'identificació personal. Això no inclou tercers de confiança que ens ajudin a operar el nostre lloc, a dur a terme el nostre negoci o a fer-ho, sempre que aquestes parts acceptin mantenir confidencial aquesta informació. També podem publicar la vostra informació quan creiem que l'alliberament és apropiat per complir amb la llei, fer complir les polítiques del nostre lloc o protegir els nostres drets o altres drets, propietat o seguretat. No obstant això, la informació de visitant que no sigui personalment identificable es pot proporcionar a altres parts per a la comercialització, la publicitat o altres usos.</p>
+
+      <h3 id="third-party">Vincles de tercers</h3>
+
+      <p>De tant en tant, segons el nostre criteri, podem incloure o oferir productes o serveis de tercers al nostre lloc. Aquests llocs de tercers tenen polítiques de privadesa separades i independents. Per tant, no tenim responsabilitat ni responsabilitat civil pel contingut i les activitats d'aquests llocs enllaçats. No obstant això, busquem protegir la integritat del nostre lloc i donem la benvinguda a qualsevol comentari sobre aquests llocs.</p>
+
+      <h3 id="coppa">Compliment de la Llei de protecció de la privacitat en línia dels nens</h3>
+
+      <p>El nostre lloc, productes i serveis estan dirigits a persones que tenen almenys 13 anys. Si aquest servidor es troba als EUA, i teniu menys de 13 anys, segons els requisits de COPPA (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>) no feu servir aquest lloc.</p>
+
+      <h3 id="online">Només la política de privacitat en línia</h3>
+
+      <p>Aquesta política de privacitat en línia només s'aplica a la informació recopilada a través del nostre lloc i no a la informació recopilada fora de línia.</p>
+
+      <h3 id="consent">El vostre consentiment</h3>
+
+      <p>En utilitzar el nostre lloc, accepta la política de privadesa del nostre lloc web.</p>
+
+      <h3 id="changes">Canvis a la nostra política de privacitat</h3>
+
+      <p>Si decidim canviar la nostra política de privadesa, publicarem aquests canvis en aquesta pàgina.</p>
+
+      <p>Aquest document és CC-BY-SA. Es va actualitzar per última vegada el 31 de maig de 2013.</p>
+
+      <p>Originalment adaptat a la <a href="https://github.com/discourse/discourse">política de privadesa del Discurs</a>.</p>
     title: "%{instance} Condicions del servei i política de privadesa"
   time:
     formats:
       default: "%b %d, %Y, %H:%M"
   two_factor_authentication:
     code_hint: Introdueix el codi generat per l'aplicació autenticadora per a confirmar
-    description_html: Si habilites la <strong>autenticació de dos factors</strong>, et caldrà tenir el teu telèfon, que generarà tokens per a que puguis iniciar sessió.
-    disable: Deshabilitarr
-    enable: Habilitar
+    description_html: Si habilites l'<strong>autenticació de dos factors</strong>, et caldrà tenir el teu telèfon, que generarà tokens per a que puguis iniciar sessió.
+    disable: Desactiva
+    enable: Activa
     enabled: Autenticació de dos factors activada
-    enabled_success: Autenticació de dos factors activada amb èxit
-    generate_recovery_codes: Generar codis de recuperació
+    enabled_success: Autenticació de dos factors activada correctament
+    generate_recovery_codes: Genera codis de recuperació
     instructions_html: "<strong>Escaneja aquest codi QR desde Google Authenticator o una aplicació similar del teu telèfon</strong>. Desde ara, aquesta aplicació generarà tokens que tens que ingresar quan volguis iniciar sessió."
-    lost_recovery_codes: Els codis de recuperació et permeten recuperar l'accés al teu compte si perds el telèfon. Si has perdut els teus codis de recuperació els pots regenerar aquí. Els codis de recuperació anteriors seran anul·lats.
-    manual_instructions: 'Si no pots escanejar el codi QR code i necessites introduir-lo manualment, aquí tens el secret en text plà:'
+    lost_recovery_codes: Els codis de recuperació et permeten recuperar l'accés al teu compte si perds el telèfon. Si has perdut els codis de recuperació els pots tornar a generar aquí. Els codis de recuperació anteriors s'anul·laran.
+    manual_instructions: 'Si no pots escanejar el codi QR code i necessites introduir-lo manualment, aquí tens el secret en text pla:'
     recovery_codes: Codis de recuperació de còpia de seguretat
     recovery_codes_regenerated: Codis de recuperació regenerats amb èxit
-    recovery_instructions_html: Si alguna vegada perds l'accéss al telèfon pots utilitzar un dels codis de recuperació a continuació per recuperar l'accés al teu compte. Cal mantenir els codis de recuperació en lloc segur, per exemple imprimint-los i guardar-los amb altres documents importants.
+    recovery_instructions_html: Si mai perds l'accéss al telèfon pots utilitzar un dels codis de recuperació a continuació per a recuperar l'accés al teu compte. Cal mantenir els codis de recuperació en lloc segur, per exemple imprimint-los i guardar-los amb altres documents importants.
     setup: Establir
-    wrong_code: El codi introduït es invalid! Es correcta la hora del servidor i del dispositiu?
+    wrong_code: El codi introduït no és vàlid! És correcta l'hora del servidor i del dispositiu?
   users:
-    invalid_email: La direcció de correu es incorrecte
-    invalid_otp_token: Codi de dos factors incorrecte
+    invalid_email: L'adreça de correu no és correcta
+    invalid_otp_token: El codi de dos factors no és correcte
     signed_in_as: 'Sessió iniciada com a:'
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 169af50ff..db96f7de7 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -517,8 +517,6 @@ de:
     pinned: Angehefteter Beitrag
     reblogged: teilte
     sensitive_content: Heikle Inhalte
-  themes:
-    default: Mastodon
   time:
     formats:
       default: "%d.%m.%Y %H:%M"
diff --git a/config/locales/devise.ca.yml b/config/locales/devise.ca.yml
index 4ff51b6e0..fe95c402d 100644
--- a/config/locales/devise.ca.yml
+++ b/config/locales/devise.ca.yml
@@ -2,60 +2,60 @@
 ca:
   devise:
     confirmations:
-      confirmed: La seva adreça de correu ha estat confirmada amb èxit.
-      send_instructions: Rebrà un correu electrònic amb instruccions sobre com confirmar la seva adreça de correu en pocs minuts.
-      send_paranoid_instructions: Si la seva adreça de correu electrònic existeix en la nostre base de dades, rebrà un correu electrònic amb instruccions sobre com confirmar la seva adreça de correu en pocs minuts.
+      confirmed: L'adreça de correu s'ha confirmat correctament.
+      send_instructions: En pocs minuts rebràs un correu electrònic amb instruccions sobre com confirmar l'adreça de correu.
+      send_paranoid_instructions: Si l'adreça de correu electrònic existeix en la nostra base de dades, en pocs minuts rebràs un correu electrònic amb instruccions sobre com confirmar l'adreça de correu.
     failure:
-      already_authenticated: Vosté ja està registrat.
-      inactive: el seu compte encara no està actiu.
-      invalid: Invàlid %{authentication_keys} o contrassenya.
-      last_attempt: Té un intent més abans que el seu compte sigui bloquejat.
-      locked: el seu compte ha estat bloquejat.
-      not_found_in_database: Invàlit %{authentication_keys} o contrassenya.
-      timeout: la sessió ha expirat. Si us plau iniciï sessió de nou per a continuar.
-      unauthenticated: Necessita iniciar sessió o registrar-se abans de continuar.
-      unconfirmed: Ha de confirmar la seva adreça de correu electrònic abans de continuar.
+      already_authenticated: Ja estàs registrat.
+      inactive: el teu compte encara no s'ha activat.
+      invalid: "%{authentication_keys} o contrasenya no són vàlids."
+      last_attempt: Tens un intent més, abans que es bloqui el compte.
+      locked: el compte s'ha blocat.
+      not_found_in_database: "%{authentication_keys} o contrasenya no vàlids."
+      timeout: la sessió ha expirat. Incia sessió una altra vegada per a continuar.
+      unauthenticated: Cal iniciar sessió o registrar-se abans de continuar.
+      unconfirmed: Has de confirmar l'adreça de correu electrònic abans de continuar.
     mailer:
       confirmation_instructions:
         subject: 'Mastodon: Instruccions de confirmació'
       password_change:
-        subject: 'Mastodon: Contrassenya canviada'
+        subject: 'Mastodon: Contrasenya canviada'
       reset_password_instructions:
         subject: 'Mastodon: Instruccions per a reiniciar contrassenya'
       unlock_instructions:
-        subject: 'Mastodon: Instruccions per a desbloquejar'
+        subject: 'Mastodon: Instruccions per a desblocar'
     omniauth_callbacks:
-      failure: No podem autentificar-lo desde %{kind} degut a "%{reason}".
-      success: Autentificat amb èxit desde el compte %{kind} .
+      failure: No podem autentificar-te desde %{kind} degut a "%{reason}".
+      success: Autentificat amb èxit des del compte %{kind} .
     passwords:
-      no_token: No pot accedir aquesta pàgina sense provenir desde el correu de reinici de contrassenya. Si ve desde el correu de reinici de contrassenya, si us pla asseguri que està emprant la adreça complerta proporcionada.
-      send_instructions: Rebrà un correu electrònic amb instruccions sobre com reiniciar la seva contrassenya en pocs minuts.
-      send_paranoid_instructions: Si el seu correu electrònic existeix en la nostre base de dades, rebrà un enllaç de recuperació de contrassenya en la seva adreça de correu en pocs minuts.
-      updated: La seva contrassenya ha estat canviada amb èxit. Ara ja està registrat.
-      updated_not_active: La seva contrassenya ha estat canviada amb èxit.
+      no_token: No pots accedir a aquesta pàgina sense provenir des del correu de restabliment de la contrasenya. Si vens des del correu de restabliment de contrasenya, assegura't que estàs emprant l'adreça completa proporcionada.
+      send_instructions: Rebràs un correu electrònic amb instruccions sobre com reiniciar la contrasenya en pocs minuts.
+      send_paranoid_instructions: Si el seu correu electrònic existeix en la nostra base de dades, rebràs un enllaç de restabliment de contrasenya en l'adreça de correu en pocs minuts.
+      updated: La contrassenya s'ha canviat correctament. Ara ja estàs registrat.
+      updated_not_active: La contrassenya s'ha canviat correctament.
     registrations:
-      destroyed: Adéu! el seu compte ha estat cancel·lat amb èxit. Esperem veure'l aviat de nou.
-      signed_up: Benvingut! S'ha registrat amb èxit.
-      signed_up_but_inactive: S´ha registrat amb èxit. No obstant, no podem identificar-lo perque el seu compte no ha estat activat encara.
-      signed_up_but_locked: S´ha registrat amb èxit. No obstant, no podem identificar-lo perque el seu compte està bloquejat.
+      destroyed: Adéu! el compte s'ha cancel·lat amb èxit. Desitgem veure't de nou aviat.
+      signed_up: Benvingut! T'has registrat amb èxit.
+      signed_up_but_inactive: T´has registrat amb èxit. No obstant, no podem identificar-te perquè el compte encara no s'ha activat.
+      signed_up_but_locked: T´has registrat amb èxit. No obstant, no podem identificar-te perquè el compte està blocat.
       signed_up_but_unconfirmed: Un missatge amb un enllaç de confirmació ha estat enviat per correu electrònic. Si us plau segueixi l'enllaç per activar el seu compte.
       update_needs_confirmation: Ha actualitzat el seu compte amb èxit, però necessitem verificar la nova adreça de correu. Si us plau comprovi el correu i segueixi l'enllaç per confirmar la nova adreça de correu.
       updated: el seu compte ha estat actualitzat amb èxit.
     sessions:
-      already_signed_out: Ha tancat la sessió amb èxit.
-      signed_in: S'ha registrat amb èxit.
-      signed_out: Ha tancat la sessió amb èxit.
+      already_signed_out: Has tancat la sessió amb èxit.
+      signed_in: T'has registrat amb èxit.
+      signed_out: Has tancat la sessió amb èxit.
     unlocks:
-      send_instructions: Rebrà un correu electrònic amb instruccions sobre com desbloquejar el seu compte en pocs minuts.
-      send_paranoid_instructions: Si el seu compte existeix, rebrà un correu electrònic amb instruccions sobre com desbloquejarla en pocs minuts.
-      unlocked: El seu compte ha estat bloquejat amb èxit. Si us plau iniciï sessió per continuar.
+      send_instructions: Rebràs un correu electrònic amb instruccions sobre com desblocar el compte en pocs minuts.
+      send_paranoid_instructions: Si el compte existeix, rebràs un correu electrònic amb instruccions sobre com desblocar-lo en pocs minuts.
+      unlocked: El compte s'ha blocat correctament. Inicia sessió per a continuar.
   errors:
     messages:
-      already_confirmed: ja ha estat confirmat, si us plau intenti iniciar sessió
-      confirmation_period_expired: necessita ser confirmat dins de %{period}, si us plau demani una nova
-      expired: ha expirat, si us plau demani una nova
-      not_found: no trobat
-      not_locked: no va ser bloquejada
+      already_confirmed: ja està confirmat. Intenta d'iniciar sessió
+      confirmation_period_expired: calia fer la confirmació dins de %{period}, torna a sol·licitar-la
+      expired: ha expirat, demana'n una altra
+      not_found: no s'ha trobat
+      not_locked: no està blocada
       not_saved:
-        one: '1 error ha prohibit aquest %{resource} de ser guardat:'
-        other: "%{count} errors va prohibir aquest %{resource} de ser guardat:"
+        one: '1 error ha impedit desar aquest %{resource}:'
+        other: "%{count} errors hab impedit desar aquest %{resource}:"
diff --git a/config/locales/devise.zh-CN.yml b/config/locales/devise.zh-CN.yml
index 0e40fcc90..5560e12b3 100644
--- a/config/locales/devise.zh-CN.yml
+++ b/config/locales/devise.zh-CN.yml
@@ -8,7 +8,7 @@ zh-CN:
     failure:
       already_authenticated: 你已经登录。
       inactive: 你还没有激活帐户。
-      invalid: " %{authentication_keys} 或密码错误。"
+      invalid: "%{authentication_keys}或密码错误。"
       last_attempt: 你还有最后一次尝试机会,再次失败你的帐户将被锁定。
       locked: 你的帐户已被锁定。
       not_found_in_database: "%{authentication_keys}或密码错误。"
@@ -53,10 +53,10 @@ zh-CN:
       unlocked: 你的帐户已成功解锁。登录以继续。
   errors:
     messages:
-      already_confirmed: 已经确认,请重新登录。
-      confirmation_period_expired: 注册帐户后须在 %{period}以内确认。请重新注册。
-      expired: 邮件确认已过期,请重新注册。
-      not_found: 找不到。
-      not_locked: 未锁定。
+      already_confirmed: 已经确认成功,请尝试登录
+      confirmation_period_expired: 必须在 %{period}以内确认。请重新发起请求
+      expired: 已过期。请重新发起请求
+      not_found: 找不到
+      not_locked: 未被锁定
       not_saved:
         other: 发生 %{count} 个错误,导致%{resource}保存失败:
diff --git a/config/locales/doorkeeper.ca.yml b/config/locales/doorkeeper.ca.yml
index 38dbbdde9..c2d2b79b9 100644
--- a/config/locales/doorkeeper.ca.yml
+++ b/config/locales/doorkeeper.ca.yml
@@ -3,7 +3,7 @@ ca:
   activerecord:
     attributes:
       doorkeeper/application:
-        name: Nombre
+        name: Nom
         redirect_uri: URI per a redirecció
     errors:
       models:
@@ -11,58 +11,58 @@ ca:
           attributes:
             redirect_uri:
               fragment_present: no pot contenir un fragment.
-              invalid_uri: ha de ser una URI válid.
-              relative_uri: ha de ser una URI absoluta.
-              secured_uri: ha de ser una URI HTTPS/SSL.
+              invalid_uri: ha de ser un URI válid.
+              relative_uri: ha de ser un URI absoluta.
+              secured_uri: ha de ser un URI HTTPS/SSL.
   doorkeeper:
     applications:
       buttons:
-        authorize: Autoritzar
-        cancel: Cancel⋅lar
-        destroy: Destruir
-        edit: Editar
-        submit: Enviar
+        authorize: Autoritza
+        cancel: Cancel⋅la
+        destroy: Destrueix
+        edit: Edita
+        submit: Envia
       confirmations:
-        destroy: Està segur?
+        destroy: Estàs segur?
       edit:
-        title: Editar aplicació
+        title: Edita l'aplicació
       form:
-        error: Uuups! Comprovi el formulari
+        error: Ep! Comprova el formulari
       help:
-        native_redirect_uri: Utilitzi %{native_redirect_uri} per a proves locals
-        redirect_uri: Utilitzi una línia per URI
-        scopes: Separi els àmbits amb espais. Deixa-ho en blanc per utilitzar els àmbits per defecte.
+        native_redirect_uri: Utilitza %{native_redirect_uri} per a proves locals
+        redirect_uri: Utilitza una línia per URI
+        scopes: Separa els àmbits amb espais. Deixa-ho en blanc per a utilitzar els àmbits per defecte.
       index:
         callback_url: Callback URL
         name: Nom
-        new: Nova aplicació
+        new: Aplicació nova
         title: Les teves aplicacions
       new:
-        title: Nova aplicació
+        title: Aplicació nova
       show:
         actions: Accions
-        application_id: Id de la aplicació
-        callback_urls: Callback urls
+        application_id: Id de l'aplicació
+        callback_urls: Callback URL
         scopes: Àmbits
         secret: Secret
         title: 'Aplicació: %{name}'
     authorizations:
       buttons:
-        authorize: Autoritzar
-        deny: Desautoritzar
+        authorize: Autoritza
+        deny: Desautoritza
       error:
-        title: Ha ocorregut un error
+        title: S'a produit un error
       new:
         able_to: Serà capaç de
-        prompt: La aplicació %{client_name} sol⋅licita tenir accés al teu compte
-        title: Es requereix autorizació
+        prompt: L'aplicació %{client_name} sol⋅licita tenir accés al teu compte
+        title: Cal autorizació
       show:
-        title: Copy this authorization code and paste it to the application.
+        title: Copia aquest coddi d'autorització i enganxa'l en l'aplicació.
     authorized_applications:
       buttons:
-        revoke: Revocar
+        revoke: Revoca
       confirmations:
-        revoke: Està segur?
+        revoke: Estàs segur?
       index:
         application: Aplicació
         created_at: Creat el
@@ -72,22 +72,22 @@ ca:
       messages:
         access_denied: El propietari del recurs o servidor de autorizació ha denegat la petició.
         credential_flow_not_configured: Les credencials de contrasenya del propietari del recurs han fallat degut a que Doorkeeper.configure.resource_owner_from_credentials està sense configurar.
-        invalid_client: La autentificación del cliente falló debido o a que es un cliente desconocido o no está incluída la autentificación del cliente o el método de autentificación no está confirmado.
-        invalid_grant: La concesión de autorización ofrecida es inválida, venció, se revocó, no coincide con la URI de redirección utilizada en la petición de autorización, o fue emitida para otro cliente.
-        invalid_redirect_uri: La URI de redirección incluida no es válida.
-        invalid_request: En la petición falta un parámetro necesario o incluye un valor de parámetro no soportado o tiene otro tipo de formato incorrecto.
-        invalid_resource_owner: Las credenciales del propietario del recurso proporcionado no son válidas, o el propietario del recurso no puede ser encontrado.
-        invalid_scope: El ámbito pedido es inválido, desconocido o erróneo.
+        invalid_client: La autentificació del client falló perquè és un client desconegut o no està inclòs l'autentificació del client o el mètode d'autenticació no està confirmat.
+        invalid_grant: La concessió d'autorizació oferida és invàlida, ha vençut, s'ha revocat, no coincideix amb l'URI de redirecció utilizada en la petició d'autorizació, o fou emesa per a un altre client.
+        invalid_redirect_uri: L'URI de redirecció inclòs no és vàlid.
+        invalid_request: En la petició manca un paràmetre necessari o inclou un valor de paràmetre no suportat o te un altre tipus de format incorrecte.
+        invalid_resource_owner: Les credencials del propietari del recurso proporcionat no son vàlides, o el propietari del recurs no pot ser trobat.
+        invalid_scope: L'àmbit demanat és invàlid, desconegut o erroni.
         invalid_token:
-          expired: El identificador de acceso finalizó.
-          revoked: El identificador de acceso fue revocado.
-          unknown: El identificador de acceso es inválido.
-        resource_owner_authenticator_not_configured: El propietario del recurso falló debido a que Doorkeeper.configure.resource_owner_authenticator está sin configurar.
-        server_error: El servidor de la autorización entontró una condición inesperada que le impidió cumplir con la solicitud.
-        temporarily_unavailable: El servidor de la autorización es actualmente incapaz de manejar la petición debido a una sobrecarga temporal o un trabajo de mantenimiento del servidor.
-        unauthorized_client: El cliente no está autorizado a realizar esta petición utilizando este método.
-        unsupported_grant_type: El tipo de concesión de autorización no está soportado por el servidor de autorización.
-        unsupported_response_type: El servidor de autorización no soporta este tipo de respuesta.
+          expired: L'identificador d'accés ha caducat.
+          revoked: L'identificador d'accés fou revocat.
+          unknown: L'identificador d'accés és invàlid.
+        resource_owner_authenticator_not_configured: El propietari del recurs ha fallat perquè Doorkeeper.configure.resource_owner_authenticator està sense configurar.
+        server_error: El servidor de l'autorizació ha trobat unca condició inesperada que ha impedit complir la sol·licitud.
+        temporarily_unavailable: El servidor de l'autorizació és actualment incapaç de gestionar la petició degut a una sobrecàrrega temporal o una tasca de manteniment del servidor.
+        unauthorized_client: El client no està autoritzat a fer aquesta petició utilitzant aquest mètode.
+        unsupported_grant_type: El tipus de concessió d'autorització no està suportat pel servidor d'autorizació.
+        unsupported_response_type: El servidor d'autorizació no suporta aquest tipus de resposta.
     flash:
       applications:
         create:
@@ -107,6 +107,6 @@ ca:
       application:
         title: OAuth autorització requerida
     scopes:
-      follow: seguir, bloquejar, desbloquejar i deixar de seguir comptes
+      follow: seguir, blocar, desblocar i deixar de seguir comptes
       read: llegir les dades del teu compte
       write: publicar en el teu nom
diff --git a/config/locales/doorkeeper.zh-CN.yml b/config/locales/doorkeeper.zh-CN.yml
index c7e8f368d..0eb5a0ab6 100644
--- a/config/locales/doorkeeper.zh-CN.yml
+++ b/config/locales/doorkeeper.zh-CN.yml
@@ -77,10 +77,10 @@ zh-CN:
         title: 已授权的应用列表
     errors:
       messages:
-        access_denied: 用户或服务器拒绝了请求
+        access_denied: 资源所有者或服务器拒绝了请求
         credential_flow_not_configured: 由于 Doorkeeper.configure.resource_owner_from_credentials 尚未配置,应用验证授权流程失败。
         invalid_client: 由于应用信息未知、未提交认证信息或使用了不支持的认证方式,认证失败
-        invalid_grant: 授权方式无效,或者登录回调地址无效、过期或已被撤销
+        invalid_grant: 授权方式无效、过期或已被撤销、与授权请求中的回调地址不一致,或使用了其他应用的回调地址
         invalid_redirect_uri: 无效的登录回调地址
         invalid_request: 请求缺少必要的参数,或者参数值、格式不正确
         invalid_resource_owner: 资源所有者认证无效,或找不到所有者
diff --git a/config/locales/en.yml b/config/locales/en.yml
index cebf704ce..0ca320e1a 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -39,6 +39,7 @@ en:
     followers: Followers
     following: Following
     media: Media
+    moved_html: "%{name} has moved to %{new_profile_link}:"
     nothing_here: There is nothing here!
     people_followed_by: People whom %{name} follows
     people_who_follow: People who follow %{name}
@@ -132,6 +133,32 @@ en:
       unsubscribe: Unsubscribe
       username: Username
       web: Web
+    action_logs:
+      actions:
+        confirm_user: "%{name} confirmed e-mail address of user %{target}"
+        create_custom_emoji: "%{name} uploaded new emoji %{target}"
+        create_domain_block: "%{name} blocked domain %{target}"
+        create_email_domain_block: "%{name} blacklisted e-mail domain %{target}"
+        demote_user: "%{name} demoted user %{target}"
+        destroy_domain_block: "%{name} unblocked domain %{target}"
+        destroy_email_domain_block: "%{name} whitelisted e-mail domain %{target}"
+        destroy_status: "%{name} removed status by %{target}"
+        disable_2fa_user: "%{name} disabled two factor requirement for user %{target}"
+        disable_custom_emoji: "%{name} disabled emoji %{target}"
+        disable_user: "%{name} disabled login for user %{target}"
+        enable_custom_emoji: "%{name} enabled emoji %{target}"
+        enable_user: "%{name} enabled login for user %{target}"
+        memorialize_account: "%{name} turned %{target}'s account into a memoriam page"
+        promote_user: "%{name} promoted user %{target}"
+        reset_password_user: "%{name} reset password of user %{target}"
+        resolve_report: "%{name} dismissed report %{target}"
+        silence_account: "%{name} silenced %{target}'s account"
+        suspend_account: "%{name} suspended %{target}'s account"
+        unsilence_account: "%{name} unsilenced %{target}'s account"
+        unsuspend_account: "%{name} unsuspended %{target}'s account"
+        update_custom_emoji: "%{name} updated emoji %{target}"
+        update_status: "%{name} updated status by %{target}"
+      title: Audit log
     custom_emojis:
       copied_msg: Successfully created local copy of the emoji
       copy: Copy
@@ -186,24 +213,31 @@ en:
           suspend: Unsuspend all existing accounts from this domain
         title: Undo domain block for %{domain}
         undo: Undo
-      title: Domain Blocks
+      title: Domain blocks
       undo: Undo
     email_domain_blocks:
       add_new: Add new
-      created_msg: Email domain block successfully created
+      created_msg: Successfully added e-mail domain to blacklist
       delete: Delete
-      destroyed_msg: Email domain block successfully deleted
+      destroyed_msg: Successfully deleted e-mail domain from blacklist
       domain: Domain
       new:
-        create: Create block
-        title: New email domain block
-      title: Email Domain Block
+        create: Add domain
+        title: New e-mail blacklist entry
+      title: E-mail blacklist
     instances:
       account_count: Known accounts
       domain_name: Domain
       reset: Reset
       search: Search
-      title: Known Instances
+      title: Known instances
+    invites:
+      filter:
+        all: All
+        available: Available
+        expired: Expired
+        title: Filter
+      title: Invites
     reports:
       action_taken_by: Action taken by
       are_you_sure: Are you sure?
@@ -242,9 +276,15 @@ en:
         deletion:
           desc_html: Allow anyone to delete their account
           title: Open account deletion
+        min_invite_role:
+          disabled: No one
+          title: Allow invitations by
         open:
           desc_html: Allow anyone to create an account
           title: Open registration
+      show_staff_badge:
+        desc_html: Show a staff badge on a user page
+        title: Show staff badge
       site_description:
         desc_html: Introductory paragraph on the frontpage and in meta tags. You can use HTML tags, in particular <code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>.
         title: Instance description
@@ -261,7 +301,7 @@ en:
       timeline_preview:
         desc_html: Display public timeline on landing page
         title: Timeline preview
-      title: Site Settings
+      title: Site settings
     statuses:
       back_to_account: Back to account page
       batch:
@@ -312,6 +352,8 @@ en:
     invalid_reset_password_token: Password reset token is invalid or expired. Please request a new one.
     login: Log in
     logout: Logout
+    migrate_account: Move to a different account
+    migrate_account_html: If you wish to redirect this account to a different one, you can <a href="%{path}">configure it here</a>.
     register: Sign up
     resend_confirmation: Resend confirmation instructions
     reset_password: Reset password
@@ -394,6 +436,26 @@ en:
       muting: Muting list
     upload: Upload
   in_memoriam_html: In Memoriam.
+  invites:
+    delete: Deactivate
+    expired: Expired
+    expires_in:
+      '1800': 30 minutes
+      '21600': 6 hours
+      '3600': 1 hour
+      '43200': 12 hours
+      '86400': 1 day
+    expires_in_prompt: Never
+    generate: Generate
+    max_uses:
+      one: 1 use
+      other: "%{count} uses"
+    max_uses_prompt: No limit
+    prompt: Generate and share links with others to grant access to this instance
+    table:
+      expires_at: Expires
+      uses: Uses
+    title: Invite people
   keyword_mutes:
     add_keyword: Add keyword
     edit: Edit
@@ -408,6 +470,13 @@ en:
     validations:
       images_and_video: Cannot attach a video to a status that already contains images
       too_many: Cannot attach more than 4 files
+  migrations:
+    acct: username@domain of the new account
+    currently_redirecting: 'Your profile is set to redirect to:'
+    proceed: Save
+    updated_msg: Your account migration setting successfully updated!
+  moderation:
+    title: Moderation
   notification_mailer:
     digest:
       body: 'Here is a brief summary of what you missed on %{instance} since your last visit on %{since}:'
@@ -521,6 +590,7 @@ en:
     followers: Authorized followers
     import: Import
     keyword_mutes: Muted keywords
+    migrate: Account migration
     notifications: Notifications
     preferences: Preferences
     settings: Settings
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index 0bf195d1b..847299ac7 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -423,8 +423,6 @@ eo:
     sensitive_content: Tikla enhavo
   terms:
     title: "%{instance} Reguloj de servo kaj Politikaj pri privatecoj"
-  themes:
-    default: Mastodon
   time:
     formats:
       default: "%b %d, %Y, %H:%M"
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index 24a2ddd51..1e3bd0e8b 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -587,8 +587,6 @@ fa:
 
       <p>این نوشته اقتباسی است از <a href="https://github.com/discourse/discourse">سیاست رازداری Discourse</a>.</p>
     title: شرایط استفاده و سیاست رازداری %{instance}
-  themes:
-    default: Mastodon
   time:
     formats:
       default: "%d %b %Y, %H:%M"
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 55588d111..2fd875b2c 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -587,8 +587,6 @@ fr:
 
       <p>Originellement adapté à partir de la politique de confidentialité de <a href="https://github.com/discourse/discourse">Discourse</a>.</p>
     title: "%{instance} Conditions d’utilisations et politique de confidentialité"
-  themes:
-    default: Mastodon
   time:
     formats:
       default: "%d %b %Y, %H:%M"
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 82b642b5b..a008d9cc4 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -39,6 +39,7 @@ ja:
     followers: フォロワー
     following: フォロー中
     media: メディア
+    moved_html: "%{name} さんは引っ越しました %{new_profile_link}:"
     nothing_here: 何もありません
     people_followed_by: "%{name} さんがフォロー中のアカウント"
     people_who_follow: "%{name} さんをフォロー中のアカウント"
@@ -48,6 +49,7 @@ ja:
     reserved_username: このユーザー名は予約されています。
     roles:
       admin: Admin
+      moderator: Mod
     unfollow: フォロー解除
   admin:
     account_moderation_notes:
@@ -131,6 +133,32 @@ ja:
       unsubscribe: 購読の解除
       username: ユーザー名
       web: Web
+    action_logs:
+      actions:
+        confirm_user: "%{name} さんが %{target} さんのメールアドレスを確認済みにしました"
+        create_custom_emoji: "%{name} さんがカスタム絵文字 %{target} を追加しました"
+        create_domain_block: "%{name} さんがドメイン %{target} をブロックしました"
+        create_email_domain_block: "%{name} さんがドメイン %{target} をメールアドレス用ブラックリストに追加しました"
+        demote_user: "%{name} さんが %{target} さんを降格しました"
+        destroy_domain_block: "%{name} さんがドメイン %{target} のブロックを外しました"
+        destroy_email_domain_block: "%{name} さんがドメイン %{target} をメールアドレス用ブラックリストから外しました"
+        destroy_status: "%{name} さんが %{target} さんの投稿を削除しました"
+        disable_2fa_user: "%{name} さんが %{target} さんの二段階認証を無効化しました"
+        disable_custom_emoji: "%{name} さんがカスタム絵文字 %{target} を無効化しました"
+        disable_user: "%{name} さんが %{target} さんのログインを無効化しました"
+        enable_custom_emoji: "%{name} さんがカスタム絵文字 %{target} を有効化しました"
+        enable_user: "%{name} さんが %{target} さんのログインを有効化しました"
+        memorialize_account: "%{name} さんが %{target} さんを追悼アカウントページに登録しました"
+        promote_user: "%{name} さんが %{target} さんを昇格しました"
+        reset_password_user: "%{name} さんが %{target} さんのパスワードをリセットしました"
+        resolve_report: "%{name} さんがレポート %{target} を棄却しました"
+        silence_account: "%{name} さんが %{target} さんをサイレンスにしました"
+        suspend_account: "%{name} さんが %{target} さんを停止しました"
+        unsilence_account: "%{name} さんが %{target} さんのサイレンスを解除しました"
+        unsuspend_account: "%{name} さんが %{target} さんの停止を解除しました"
+        update_custom_emoji: "%{name} さんがカスタム絵文字 %{target} を更新しました"
+        update_status: "%{name} さんが %{target} さんの投稿を更新しました"
+      title: 操作履歴
     custom_emojis:
       copied_msg: 絵文字のコピーをローカルに作成しました
       copy: コピー
@@ -189,20 +217,27 @@ ja:
       undo: 元に戻す
     email_domain_blocks:
       add_new: 新規追加
-      created_msg: 処理を完了しました
+      created_msg: ブラックリストに追加しました
       delete: 消去
-      destroyed_msg: 消去しました
+      destroyed_msg: ブラックリストから外しました
       domain: ドメイン
       new:
-        create: ブロックを作成
-        title: 新規メールドメインブロック
-      title: メールドメインブロック
+        create: ドメインを追加
+        title: メールアドレス用ブラックリスト新規追加
+      title: メールブラックリスト
     instances:
       account_count: 既知のアカウント数
       domain_name: ドメイン名
       reset: リセット
       search: 検索
       title: 既知のインスタンス
+    invites:
+      filter:
+        all: すべて
+        available: 使用可能
+        expired: 期限切れ
+        title: フィルター
+      title: 招待
     reports:
       action_taken_by: レポート処理者
       are_you_sure: 本当に実行しますか?
@@ -241,9 +276,15 @@ ja:
         deletion:
           desc_html: 誰でも自分のアカウントを削除できるようにします
           title: アカウント削除を受け付ける
+        min_invite_role:
+          disabled: 誰も許可しない
+          title: 招待の作成を許可
         open:
           desc_html: 誰でも自由にアカウントを作成できるようにします
           title: 新規登録を受け付ける
+      show_staff_badge:
+        desc_html: ユーザーページにスタッフのバッジを表示します
+        title: スタッフバッジを表示する
       site_description:
         desc_html: フロントページへの表示と meta タグに使用される紹介文です。HTMLタグ、特に<code>&lt;a&gt;</code> と <code>&lt;em&gt;</code>が使えます。
         title: インスタンスの説明
@@ -311,6 +352,8 @@ ja:
     invalid_reset_password_token: パスワードリセットトークンが正しくないか期限切れです。もう一度リクエストしてください。
     login: ログイン
     logout: ログアウト
+    migrate_account: 別のアカウントに引っ越す
+    migrate_account_html: 引っ越し先を明記したい場合は<a href="%{path}">こちら</a>で設定できます。
     register: 登録する
     resend_confirmation: 確認メールを再送する
     reset_password: パスワードを再発行
@@ -393,12 +436,39 @@ ja:
       muting: ミュートしたアカウントリスト
     upload: アップロード
   in_memoriam_html: 故人を偲んで
+  invites:
+    delete: 無効化
+    expired: 期限切れ
+    expires_in:
+      '1800': 30 分
+      '21600': 6 時間
+      '3600': 1 時間
+      '43200': 12 時間
+      '86400': 1 日
+    expires_in_prompt: 無期限
+    generate: 作成
+    max_uses:
+      one: 1
+      other: "%{count}"
+    max_uses_prompt: 無制限
+    prompt: リンクを生成・共有してこのインスタンスへの新規登録を受け付けることができます。
+    table:
+      expires_at: 有効期限
+      uses: 使用
+    title: 新規ユーザーの招待
   landing_strip_html: "<strong>%{name}</strong> さんはインスタンス %{link_to_root_path} のユーザーです。アカウントさえ持っていればフォローしたり会話したりできます。"
   landing_strip_signup_html: もしお持ちでないなら <a href="%{sign_up_path}">こちら</a> からサインアップできます。
   media_attachments:
     validations:
       images_and_video: 既に画像が追加されているため、動画を追加することはできません。
       too_many: 追加できるファイルは4つまでです。
+  migrations:
+    acct: 引っ越し先の ユーザー名@ドメイン
+    currently_redirecting: 'あなたのプロフィールは引っ越し先が設定されています:'
+    proceed: 保存
+    updated_msg: アカウントの引っ越し設定を更新しました
+  moderation:
+    title: モデレーション
   notification_mailer:
     digest:
       body: "%{instance} での最後のログインからの出来事:"
@@ -511,6 +581,7 @@ ja:
     export: データのエクスポート
     followers: 信頼済みのインスタンス
     import: データのインポート
+    migrate: アカウントの引っ越し
     notifications: 通知
     preferences: ユーザー設定
     settings: 設定
@@ -607,8 +678,6 @@ ja:
 
       <p>オリジナルの出典 <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
     title: "%{instance} 利用規約・プライバシーポリシー"
-  themes:
-    default: Mastodon
   time:
     formats:
       default: "%Y年%m月%d日 %H:%M"
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index abf5f0ea4..fd6351486 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -518,8 +518,6 @@ ko:
     sensitive_content: 민감한 컨텐츠
   terms:
     title: "%{instance} 이용약관과 개인정보 취급 방침"
-  themes:
-    default: Mastodon
   time:
     formats:
       default: "%Y년 %m월 %d일 %H:%M"
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 501ec013d..cda771ce2 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -587,8 +587,6 @@ nl:
 
       <p>Originally adapted from the <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
     title: "%{instance} Terms of Service and Privacy Policy"
-  themes:
-    default: Mastodon
   time:
     formats:
       default: "%d %B %Y om %H:%M"
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index 914cc7e9d..e5d036303 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -678,8 +678,6 @@ oc:
 
       <p>Prima adaptacion de la <a href="https://github.com/discourse/discourse">politica de confidencialitat de Discourse</a>.</p>
     title: Condicions d’utilizacion e politica de confidencialitat de %{instance}
-  themes:
-    default: Mastodon
   time:
     formats:
       default: Lo %d %b de %Y a %Ho%M
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 49dace354..19ee154ab 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -39,6 +39,7 @@ pl:
     followers: Śledzących
     following: Śledzi
     media: Zawartość multimedialna
+    moved_html: "%{name} korzysta teraz z konta %{new_profile_link}:"
     nothing_here: Niczego tu nie ma!
     people_followed_by: Konta śledzone przez %{name}
     people_who_follow: Osoby, które śledzą konto %{name}
@@ -48,6 +49,7 @@ pl:
     reserved_username: Ta nazwa użytkownika jest zarezerwowana.
     roles:
       admin: Administrator
+      moderator: Moderator
     unfollow: Przestań śledzić
   admin:
     account_moderation_notes:
@@ -131,6 +133,32 @@ pl:
       unsubscribe: Przestań subskrybować
       username: Nazwa użytkownika
       web: Sieć
+    action_logs:
+      actions:
+        confirm_user: "%{name} potwierdził adres e-mail użytkownika %{target}"
+        create_custom_emoji: "%{name} dodał nowe emoji %{target}"
+        create_domain_block: "%{name} zablokował domenę %{target}"
+        create_email_domain_block: "%{name} dodał domenę e-mail %{target} na czarną listę"
+        demote_user: "%{name} zdegradował użytkownika %{target}"
+        destroy_domain_block: "%{name} odblokował domenę %{target}"
+        destroy_email_domain_block: "%{name} usunął domenę e-mail %{target} z czarnej listy"
+        destroy_status: "%{name} usunął wpis użytkownika %{target}"
+        disable_2fa_user: "%{name} wyłączył uwierzytelnianie dwustopniowe użytkownikowi %{target}"
+        disable_custom_emoji: "%{name} wyłączył emoji %{target}"
+        disable_user: "%{name} zablokował możliwość logowania użytkownikowi %{target}"
+        enable_custom_emoji: "%{name} włączył emoji %{target}"
+        enable_user: "%{name} przywrócił możliwość logowania użytkownikowi %{target}"
+        memorialize_account: "%{name} nadał kontu %{target} status in memoriam"
+        promote_user: "%{name} podniósł uprawnienia użytkownikowi %{target}"
+        reset_password_user: "%{name} przywrócił hasło użytkownikowi %{target}"
+        resolve_report: "%{name} odrzucił zgłoszenie %{target}"
+        silence_account: "%{name} wyciszył konto %{target}"
+        suspend_account: "%{name} zawiesił konto %{target}"
+        unsilence_account: "%{name} cofnął wyciszenie konta %{target}"
+        unsuspend_account: "%{name} cofnął zawieszenie konta %{target}"
+        update_custom_emoji: "%{name} zaktualizował emoji %{target}"
+        update_status: "%{name} zaktualizował wpis użytkownika %{target}"
+      title: Dziennik działań administracyjnych
     custom_emojis:
       copied_msg: Pomyślnie utworzono lokalną kopię emoji
       copy: Kopiuj
@@ -147,6 +175,7 @@ pl:
       listed: Widoczne
       new:
         title: Dodaj nowe niestandardowe emoji
+      overwrite: Zastąp
       shortcode: Shortcode
       shortcode_hint: Co najmniej 2 znaki, tylko znaki alfanumeryczne i podkreślniki
       title: Niestandardowe emoji
@@ -203,6 +232,13 @@ pl:
       reset: Przywróć
       search: Szukaj
       title: Znane instancje
+    invites:
+      filter:
+        all: Wszystkie
+        available: Dostępne
+        expired: Wygasłe
+        title: Filtruj
+      title: Zaproszenia
     reports:
       action_taken_by: Działanie podjęte przez
       are_you_sure: Czy na pewno?
@@ -244,6 +280,9 @@ pl:
         open:
           desc_html: Pozwól każdemu na założenie konta
           title: Otwarta rejestracja
+      show_staff_badge:
+        desc_html: Pokazuj odznakę uprawnień na stronie profilu użytkownika
+        title: Pokazuj odznakę administracji
       site_description:
         desc_html: Akapit wprowadzający, widoczny na stronie głównej i znacznikach meta. Możesz korzystać z tagów HTML, w szczególności <code>&lt;a&gt;</code> i <code>&lt;em&gt;</code>.
         title: Opis instancji
@@ -311,6 +350,8 @@ pl:
     invalid_reset_password_token: Token do resetowania hasła jest nieprawidłowy lub utracił ważność. Spróbuj uzyskać nowy.
     login: Zaloguj się
     logout: Wyloguj się
+    migrate_account: Przenieś konto
+    migrate_account_html: Jeżeli chcesz skonfigurować przekierowanie z obecnego konta na inne, możesz <a href="%{path}">skonfigurować to tutaj</a>.
     register: Rejestracja
     resend_confirmation: Ponownie prześlij instrukcje weryfikacji
     reset_password: Zresetuj hasło
@@ -393,12 +434,48 @@ pl:
       muting: Lista wyciszonych
     upload: Załaduj
   in_memoriam_html: Ku pamięci.
+  invites:
+    delete: Wygaś
+    expired: Wygasły
+    expires_in:
+      '1800': 30 minutach
+      '21600': 6 godzinach
+      '3600': godzinie
+      '43200': 12 godzinach
+      '86400': dobie
+    expires_in_prompt: Nigdy
+    generate: Wygeneruj
+    max_uses:
+      few: "%{count} użycia"
+      many: "%{count} użyć"
+      one: jedno użycie
+      other: "%{count} użyć"
+    max_uses_prompt: Bez ograniczenia
+    prompt: Wygeneruj odnośniki i udostępnij je innym, aby pozwolić na rejestrację na instancji
+    table:
+      expires_at: Wygaśnie po
+      uses: Użycia
+    title: Zaproś użytkowników
+  keyword_mutes:
+    add_keyword: Dodaj słowo kluczowe
+    edit: Edytuj
+    edit_keyword: Edytuj słowo kluczowe
+    keyword: Słowo kluczowe
+    match_whole_word: Uwzględniaj całe słowo
+    remove: Usuń
+    remove_all: Usuń wszystkie
   landing_strip_html: "<strong>%{name}</strong> ma konto na %{link_to_root_path}. Możesz je śledzić i wejść z nim w interakcję jeśli masz konto gdziekolwiek w Fediwersum."
   landing_strip_signup_html: Jeśli jeszcze go nie masz, możesz <a href="%{sign_up_path}">stworzyć konto</a>.
   media_attachments:
     validations:
       images_and_video: Nie możesz załączyć pliku wideo do wpisu, który zawiera już zdjęcia
       too_many: Nie możesz załączyć więcej niż 4 plików
+  migrations:
+    acct: nazwa@domena nowego konta
+    currently_redirecting: 'Obecnie Twoje konto przekierowuje do:'
+    proceed: Zapisz
+  moderation:
+    title: Moderacja
   notification_mailer:
     digest:
       body: 'Oto krótkie podsumowanie co Cię ominęło na %{instance} od Twojej ostatniej wizyty (%{since}):'
@@ -515,6 +592,7 @@ pl:
     export: Eksportowanie danych
     followers: Autoryzowani śledzący
     import: Importowanie danych
+    migrate: Migracja konta
     notifications: Powiadomienia
     preferences: Preferencje
     settings: Ustawienia
@@ -611,8 +689,6 @@ pl:
 
       <p>Tekst bazuje na <a href="https://github.com/discourse/discourse">polityce prywatności Discourse</a>.</p>
     title: Zasady korzystania i polityka prywatności %{instance}
-  themes:
-    default: Mastodon
   time:
     formats:
       default: "%b %d, %Y, %H:%M"
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index f5c61c01c..de2b9c778 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -607,8 +607,6 @@ pt-BR:
 
       <p>Originalmente adaptado da <a href="https://github.com/discourse/discourse">política de privacidade do Discourse</a>.</p>
     title: "%{instance} Termos de Serviço e Política de Privacidade"
-  themes:
-    default: Mastodon
   time:
     formats:
       default: "%b %d, %Y, %H:%M"
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 7c9caec14..5eb7f256a 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -521,8 +521,6 @@ ru:
     sensitive_content: Чувствительный контент
   terms:
     title: Условия обслуживания и политика конфиденциальности %{instance}
-  themes:
-    default: Mastodon
   time:
     formats:
       default: "%b %d, %Y, %H:%M"
diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml
index b5b7f26d2..f8402af21 100644
--- a/config/locales/simple_form.ca.yml
+++ b/config/locales/simple_form.ca.yml
@@ -4,14 +4,17 @@ ca:
     hints:
       defaults:
         avatar: PNG, GIF o JPG. Màxim 2MB. Serà escalat a 120x120px
+        digest: S'envia després d'un llarg període d'inactivitat amb un resum de les mencions que has rebut en la teva absència
         display_name:
-          one: <span class="name-counter">1</span> character left
-          other: <span class="name-counter">%{count}</span> characters left
+          one: <span class="name-counter">1</span> càracter
+          other: <span class="name-counter">%{count}</span> càracters
         header: PNG, GIF o JPG. Màxim 2MB. Serà escalat a 700x335px
         locked: Requereix que aprovis manualment seguidors i les publicacions seran mostrades només als teus seguidors
         note:
-          one: <span class="note-counter">1</span> character left
-          other: <span class="note-counter">%{count}</span> characters left
+          one: <span class="note-counter">1</span> càracter left
+          other: <span class="note-counter">%{count}</span> càracters
+        setting_noindex: Afecta el teu perfil públic i les pàgines d'estat
+        setting_theme: Afecta la manera en què Mastodon es veu quan està connectat des de qualsevol dispositiu.
       imports:
         data: Arxiu CSV exportat desde una altra instància de Mastodon
       sessions:
@@ -27,6 +30,7 @@ ca:
         data: Informació
         display_name: Mostrar nom
         email: Direcció de correu electrònic
+        filtered_languages: Idiomes filtrats
         header: Img. capçalera
         locale: Idioma
         locked: Fer privat aquest compte
@@ -37,13 +41,20 @@ ca:
         setting_auto_play_gif: Auto-reproducció de GIFs animats
         setting_boost_modal: Mostrar finestra de confirmació abans d'un Retoot
         setting_default_privacy: Privacitat de publicacions
+        setting_default_sensitive: Marca sempre els multimèdia com a sensibles
         setting_delete_modal: Mostrar finestra de confirmació abans d'esborrar un toot
+        setting_noindex: Desactivació de la indexació del motor de cerca
+        setting_reduce_motion: Redueix el moviment en animacions
+        setting_system_font_ui: Utilitzeu el tipus de lletra predeterminat del sistema
+        setting_theme: Tema del lloc
+        setting_unfollow_modal: Mostra el diàleg de confirmació abans de deixar de seguir a algú
         severity: Severitat
         type: Importar tipus
         username: Nom d´usuari
       interactions:
         must_be_follower: Bloquejar notificacions de persones que no et segueixen
         must_be_following: Bloquejar notificacions de persones que no segueixes
+        must_be_following_dm: Bloqueja missatges directes de persones que no segueixes
       notification_emails:
         digest: Enviar resum de correus electrònics
         favourite: Enviar correu electrònic quan algú marqui com a favorit en la teva publicació
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 66ec13634..35b45fbc9 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -31,10 +31,12 @@ en:
         data: Data
         display_name: Display name
         email: E-mail address
+        expires_in: Expire after
         filtered_languages: Filtered languages
         header: Header
         locale: Language
         locked: Lock account
+        max_uses: Max number of uses
         new_password: New password
         note: Bio
         otp_attempt: Two-factor code
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index 48eaf79b2..bdeefa7e5 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -26,10 +26,12 @@ ja:
         data: データ
         display_name: 表示名
         email: メールアドレス
+        expires_in: 有効期限
         filtered_languages: 除外する言語
         header: ヘッダー
         locale: 言語
         locked: 非公開アカウントにする
+        max_uses: 使用できる回数
         new_password: 新しいパスワード
         note: プロフィール
         otp_attempt: 二段階認証コード
diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml
index 8b539662c..507e46469 100644
--- a/config/locales/simple_form.pl.yml
+++ b/config/locales/simple_form.pl.yml
@@ -34,10 +34,12 @@ pl:
         data: Dane
         display_name: Widoczna nazwa
         email: Adres e-mail
+        expires_in: Wygaśnie po
         filtered_languages: Filtrowane języki
         header: Nagłówek
         locale: Język
         locked: Ustaw konto jako prywatne
+        max_uses: Maksymalna liczba użyć
         new_password: Nowe hasło
         note: Biogram
         otp_attempt: Kod uwierzytelnienia dwustopniowego
diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml
index f8c9461ad..23a1f59da 100644
--- a/config/locales/simple_form.zh-CN.yml
+++ b/config/locales/simple_form.zh-CN.yml
@@ -50,6 +50,7 @@ zh-CN:
       interactions:
         must_be_follower: 屏蔽来自未关注你的用户的通知
         must_be_following: 屏蔽来自你未关注的用户的通知
+        must_be_following_dm: 屏蔽来自你未关注的用户的私信
       notification_emails:
         digest: 发送摘要邮件
         favourite: 当有用户收藏了你的嘟文时,发送电子邮件提醒我
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index 260b44666..ebb6d6595 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -587,8 +587,6 @@ sv:
 
       <p>Ursprungligen anpassad från <a href="https://github.com/discourse/discourse">Discourse integritetspolicy</a>.</p>
     title: "%{instance} Användarvillkor och Sekretesspolicy"
-  themes:
-    default: Mastodon
   time:
     formats:
       default: "%b %d, %Y, %H:%M"
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index b50c34fd0..ec913113f 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -1,7 +1,7 @@
 ---
 zh-CN:
   about:
-    about_hashtag_html: 这里展示的是带有话题标签 <strong>#%{hashtag}</strong> 的公开嘟文。如果你在象毛世界中拥有一个帐户,就可以加入讨论。
+    about_hashtag_html: 这里展示的是带有话题标签 <strong>#%{hashtag}</strong> 的公开嘟文。如果你想与他们互动,你需要在任意一个 Mastodon 实例或与其兼容的网站上拥有一个帐户。
     about_mastodon_html: Mastodon(长毛象)是一个基于开放式网络协议和自由、开源软件建立的社交网络,有着类似于电子邮件的分布式设计。
     about_this: 关于本实例
     closed_registrations: 这个实例目前没有开放注册。不过,你可以前往其他实例注册一个帐户,同样可以加入到这个网络中哦!
@@ -39,15 +39,17 @@ zh-CN:
     followers: 关注者
     following: 正在关注
     media: 媒体
+    moved_html: "%{name} 已经迁移到 %{new_profile_link}:"
     nothing_here: 这里神马都没有!
-    people_followed_by: 正关注
-    people_who_follow: 粉丝
+    people_followed_by: "%{name} 关注的人"
+    people_who_follow: 关注 %{name} 的人
     posts: 嘟文
     posts_with_replies: 嘟文和回复
     remote_follow: 跨站关注
     reserved_username: 此用户名已保留
     roles:
       admin: 管理员
+      moderator: 协管
     unfollow: 取消关注
   admin:
     account_moderation_notes:
@@ -88,7 +90,7 @@ zh-CN:
       memorialize: 设置为追悼帐户
       moderation:
         all: 全部
-        silenced: 已静音
+        silenced: 已隐藏
         suspended: 已封禁
         title: 帐户状态
       moderation_notes: 管理记录
@@ -122,11 +124,11 @@ zh-CN:
         created_reports: 这个帐户提交的举报
         report: 个举报
         targeted_reports: 针对这个帐户的举报
-      silence: 静音
+      silence: 隐藏
       statuses: 嘟文
       subscribe: 订阅
       title: 用户
-      undo_silenced: 解除静音
+      undo_silenced: 解除隐藏
       undo_suspension: 解除封禁
       unsubscribe: 取消订阅
       username: 用户名
@@ -164,23 +166,23 @@ zh-CN:
         create: 添加域名屏蔽
         hint: 域名屏蔽不会阻止该域名下的帐户进入本站的数据库,但是会对来自这个域名的帐户自动进行预先设置的管理操作。
         severity:
-          desc_html: 选择<strong>自动静音</strong>会将该域名下帐户发送的嘟文设置为仅关注者可见;选择<strong>自动封禁</strong>会将该域名下帐户发送的嘟文、媒体文件以及个人资料数据从本实例上删除;如果你只是想拒绝接收来自该域名的任何媒体文件,请选择<strong>无</strong>。
+          desc_html: 选择<strong>自动隐藏</strong>会将该域名下帐户发送的嘟文设置为仅关注者可见;选择<strong>自动封禁</strong>会将该域名下帐户发送的嘟文、媒体文件以及个人资料数据从本实例上删除;如果你只是想拒绝接收来自该域名的任何媒体文件,请选择<strong>无</strong>。
           noop: 无
-          silence: 自动静音
+          silence: 自动隐藏
           suspend: 自动封禁
         title: 添加域名屏蔽
       reject_media: 拒绝接收媒体文件
       reject_media_hint: 删除本地已缓存的媒体文件,并且不再接收来自该域名的任何媒体文件。此选项不影响封禁
       severities:
         noop: 无
-        silence: 自动静音
+        silence: 自动隐藏
         suspend: 自动封禁
       severity: 屏蔽级别
       show:
         affected_accounts: 将会影响到数据库中的 %{count} 个帐户
         retroactive:
-          silence: 对此域名的所有帐户取消静音
-          suspend: 对此域名的所有帐户取消封禁
+          silence: 对此域名的所有帐户解除隐藏
+          suspend: 对此域名的所有帐户解除封禁
         title: 撤销对 %{domain} 的域名屏蔽
         undo: 撤销
       title: 域名屏蔽
@@ -218,7 +220,7 @@ zh-CN:
       reported_account: 举报用户
       reported_by: 举报人
       resolved: 已处理
-      silence_account: 静音用户
+      silence_account: 隐藏用户
       status: 状态
       suspend_account: 封禁用户
       target: 被举报人
@@ -242,6 +244,9 @@ zh-CN:
         open:
           desc_html: 允许任何人建立一个帐户
           title: 开放注册
+      show_staff_badge:
+        desc_html: 在个人资料页上显示管理员标志
+        title: 显示管理员标志
       site_description:
         desc_html: 展示在首页以及 meta 标签中的网站简介。可以使用 HTML 标签,包括 <code>&lt;a&gt;</code> 和 <code>&lt;em&gt;</code>。
         title: 本站简介
@@ -361,7 +366,7 @@ zh-CN:
     blocks: 屏蔽的用户
     csv: CSV
     follows: 关注的用户
-    mutes: 静音的用户
+    mutes: 隐藏的用户
     storage: 媒体文件存储
   followers:
     domain: 域名
@@ -386,10 +391,10 @@ zh-CN:
     types:
       blocking: 屏蔽列表
       following: 关注列表
-      muting: 静音列表
+      muting: 隐藏列表
     upload: 上传
   in_memoriam_html: 谨此悼念。
-  landing_strip_html: "<strong>%{name}</strong> 是一位来自 %{link_to_root_path} 的用户。如果你想关注这个人或者与这个人互动,你需要在任意一个 Mastodon 实例或与其兼容的网站上拥有一个帐户。"
+  landing_strip_html: "<strong>%{name}</strong> 是一位来自 %{link_to_root_path} 的用户。如果你想关注他们或者与他们互动,你需要在任意一个 Mastodon 实例或与其兼容的网站上拥有一个帐户。"
   landing_strip_signup_html: 还没有这种帐户?你可以<a href="%{sign_up_path}">在本站注册一个</a>。
   media_attachments:
     validations:
@@ -601,8 +606,6 @@ zh-CN:
 
       <p>原文出自 <a href="https://github.com/discourse/discourse">Discourse 隐私权政策</a>。</p>
     title: "%{instance} 使用条款和隐私权政策"
-  themes:
-    default: Mastodon
   time:
     formats:
       default: "%Y年%-m月%d日 %H:%M"
diff --git a/config/navigation.rb b/config/navigation.rb
index 16c48849b..3f4c00dfa 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -5,7 +5,7 @@ SimpleNavigation::Configuration.run do |navigation|
     primary.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_url
 
     primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings|
-      settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url
+      settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url, highlights_on: %r{/settings/profile|/settings/migration}
       settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url
       settings.item :keyword_mutes, safe_join([fa_icon('volume-off fw'), t('settings.keyword_mutes')]), settings_keyword_mutes_url
       settings.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_notifications_url
@@ -17,21 +17,28 @@ SimpleNavigation::Configuration.run do |navigation|
       settings.item :follower_domains, safe_join([fa_icon('users fw'), t('settings.followers')]), settings_follower_domains_url
     end
 
+    primary.item :invites, safe_join([fa_icon('user-plus fw'), t('invites.title')]), invites_path, if: proc { Setting.min_invite_role == 'user' }
+
     primary.item :development, safe_join([fa_icon('code fw'), t('settings.development')]), settings_applications_url do |development|
       development.item :your_apps, safe_join([fa_icon('list fw'), t('settings.your_apps')]), settings_applications_url, highlights_on: %r{/settings/applications}
     end
 
-    primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_reports_url, if: proc { current_user.staff? } do |admin|
+    primary.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), admin_reports_url, if: proc { current_user.staff? } do |admin|
+      admin.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url
       admin.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports}
       admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts}
+      admin.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path
       admin.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url, highlights_on: %r{/admin/instances}, if: -> { current_user.admin? }
-      admin.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? }
       admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}, if: -> { current_user.admin? }
       admin.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? }
-      admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? }
-      admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? }
+    end
+
+    primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), proc { current_user.admin? ? edit_admin_settings_url : admin_custom_emojis_url }, if: proc { current_user.staff? } do |admin|
       admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }
       admin.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis}
+      admin.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? }
+      admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? }
+      admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? }
     end
 
     primary.item :logout, safe_join([fa_icon('sign-out fw'), t('auth.logout')]), destroy_user_session_url, link_html: { 'data-method' => 'delete' }
diff --git a/config/routes.rb b/config/routes.rb
index 7812e4b36..a41e76c2c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -22,6 +22,10 @@ Rails.application.routes.draw do
   get 'manifest', to: 'manifests#show', defaults: { format: 'json' }
   get 'intent', to: 'intents#show'
 
+  devise_scope :user do
+    get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite
+  end
+
   devise_for :users, path: 'auth', controllers: {
     sessions:           'auth/sessions',
     registrations:      'auth/registrations',
@@ -99,6 +103,7 @@ Rails.application.routes.draw do
     end
 
     resource :delete, only: [:show, :destroy]
+    resource :migration, only: [:show, :update]
 
     resources :sessions, only: [:destroy]
   end
@@ -106,6 +111,7 @@ Rails.application.routes.draw do
   resources :media,  only: [:show]
   resources :tags,   only: [:show]
   resources :emojis, only: [:show]
+  resources :invites, only: [:index, :create, :destroy]
 
   get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy
 
@@ -117,7 +123,9 @@ Rails.application.routes.draw do
     resources :subscriptions, only: [:index]
     resources :domain_blocks, only: [:index, :new, :create, :show, :destroy]
     resources :email_domain_blocks, only: [:index, :new, :create, :destroy]
+    resources :action_logs, only: [:index]
     resource :settings, only: [:edit, :update]
+    resources :invites, only: [:index, :create, :destroy]
 
     resources :instances, only: [:index] do
       collection do
diff --git a/config/settings.yml b/config/settings.yml
index 48d971d44..5aad45da2 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -17,6 +17,8 @@ defaults: &defaults
   closed_registrations_message: ''
   open_deletion: true
   timeline_preview: false
+  min_invite_role: 'admin'
+  show_staff_badge: true
   default_sensitive: false
   unfollow_modal: false
   boost_modal: false
diff --git a/db/migrate/20171028221157_add_reblogs_to_follows.rb b/db/migrate/20171028221157_add_reblogs_to_follows.rb
index eb4640a20..4b5d5b7ff 100644
--- a/db/migrate/20171028221157_add_reblogs_to_follows.rb
+++ b/db/migrate/20171028221157_add_reblogs_to_follows.rb
@@ -3,9 +3,7 @@ require Rails.root.join('lib', 'mastodon', 'migration_helpers')
 class AddReblogsToFollows < ActiveRecord::Migration[5.1]
   include Mastodon::MigrationHelpers
 
-  safety_assured do
-    disable_ddl_transaction!
-  end
+  disable_ddl_transaction!
 
   def up
     safety_assured do
diff --git a/db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb b/db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb
new file mode 100644
index 000000000..0c8a894cc
--- /dev/null
+++ b/db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb
@@ -0,0 +1,6 @@
+class AddMovedToAccountIdToAccounts < ActiveRecord::Migration[5.1]
+  def change
+    add_column :accounts, :moved_to_account_id, :bigint, null: true, default: nil
+    add_foreign_key :accounts, :accounts, column: :moved_to_account_id, on_delete: :nullify
+  end
+end
diff --git a/db/migrate/20171119172437_create_admin_action_logs.rb b/db/migrate/20171119172437_create_admin_action_logs.rb
new file mode 100644
index 000000000..0c2b6c623
--- /dev/null
+++ b/db/migrate/20171119172437_create_admin_action_logs.rb
@@ -0,0 +1,12 @@
+class CreateAdminActionLogs < ActiveRecord::Migration[5.1]
+  def change
+    create_table :admin_action_logs do |t|
+      t.belongs_to :account, foreign_key: { on_delete: :cascade }
+      t.string :action, null: false, default: ''
+      t.references :target, polymorphic: true
+      t.text :recorded_changes, null: false, default: ''
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb b/db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb
new file mode 100644
index 000000000..131e54b72
--- /dev/null
+++ b/db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb
@@ -0,0 +1,12 @@
+class AddIndexAccountAndReblogOfIdToStatuses < ActiveRecord::Migration[5.1]
+  disable_ddl_transaction!
+
+  def up
+    # This index has been superseded by migration 20171125185353
+    # add_index :statuses, [:account_id, :reblog_of_id], algorithm: :concurrently
+  end
+
+  def down
+    remove_index :statuses, [:account_id, :reblog_of_id] if index_exists?(:statuses, [:account_id, :reblog_of_id])
+  end
+end
diff --git a/db/migrate/20171125024930_create_invites.rb b/db/migrate/20171125024930_create_invites.rb
new file mode 100644
index 000000000..bcf03bd72
--- /dev/null
+++ b/db/migrate/20171125024930_create_invites.rb
@@ -0,0 +1,15 @@
+class CreateInvites < ActiveRecord::Migration[5.1]
+  def change
+    create_table :invites do |t|
+      t.belongs_to :user, foreign_key: { on_delete: :cascade }
+      t.string :code, null: false, default: ''
+      t.datetime :expires_at, null: true, default: nil
+      t.integer :max_uses, null: true, default: nil
+      t.integer :uses, null: false, default: 0
+
+      t.timestamps
+    end
+
+    add_index :invites, :code, unique: true
+  end
+end
diff --git a/db/migrate/20171125031751_add_invite_id_to_users.rb b/db/migrate/20171125031751_add_invite_id_to_users.rb
new file mode 100644
index 000000000..16829f866
--- /dev/null
+++ b/db/migrate/20171125031751_add_invite_id_to_users.rb
@@ -0,0 +1,5 @@
+class AddInviteIdToUsers < ActiveRecord::Migration[5.1]
+  def change
+    add_reference :users, :invite, null: true, default: nil, foreign_key: { on_delete: :nullify }, index: false
+  end
+end
diff --git a/db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb b/db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb
new file mode 100644
index 000000000..37662eaa5
--- /dev/null
+++ b/db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb
@@ -0,0 +1,7 @@
+class AddIndexReblogOfIdAndAccountToStatuses < ActiveRecord::Migration[5.1]
+  disable_ddl_transaction!
+
+  def change
+    add_index :statuses, [:reblog_of_id, :account_id], algorithm: :concurrently
+  end
+end
diff --git a/db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb b/db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb
new file mode 100644
index 000000000..68146c5ce
--- /dev/null
+++ b/db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb
@@ -0,0 +1,14 @@
+class RemoveOldReblogIndexOnStatuses < ActiveRecord::Migration[5.1]
+  disable_ddl_transaction!
+
+  def up
+    # This index may not exists (see migration 20171122120436)
+    remove_index :statuses, [:account_id, :reblog_of_id] if index_exists?(:statuses, [:account_id, :reblog_of_id])
+
+    remove_index :statuses, :reblog_of_id
+  end
+
+  def down
+    add_index :statuses, :reblog_of_id, algorithm: :concurrently
+  end
+end
diff --git a/db/migrate/20171129172043_add_index_on_stream_entries.rb b/db/migrate/20171129172043_add_index_on_stream_entries.rb
new file mode 100644
index 000000000..478530c7f
--- /dev/null
+++ b/db/migrate/20171129172043_add_index_on_stream_entries.rb
@@ -0,0 +1,7 @@
+class AddIndexOnStreamEntries < ActiveRecord::Migration[5.1]
+  def change
+    commit_db_transaction
+    add_index :stream_entries, [:account_id, :activity_type, :id], algorithm: :concurrently
+    remove_index :stream_entries, name: :index_stream_entries_on_account_id
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 10e35cd7d..c87c9b393 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20171116161857) do
+ActiveRecord::Schema.define(version: 20171129172043) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -72,6 +72,7 @@ ActiveRecord::Schema.define(version: 20171116161857) do
     t.string "followers_url", default: "", null: false
     t.integer "protocol", default: 0, null: false
     t.boolean "memorial", default: false, null: false
+    t.bigint "moved_to_account_id"
     t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
     t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower"
     t.index ["uri"], name: "index_accounts_on_uri"
@@ -79,6 +80,18 @@ ActiveRecord::Schema.define(version: 20171116161857) do
     t.index ["username", "domain"], name: "index_accounts_on_username_and_domain", unique: true
   end
 
+  create_table "admin_action_logs", force: :cascade do |t|
+    t.bigint "account_id"
+    t.string "action", default: "", null: false
+    t.string "target_type"
+    t.bigint "target_id"
+    t.text "recorded_changes", default: "", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["account_id"], name: "index_admin_action_logs_on_account_id"
+    t.index ["target_type", "target_id"], name: "index_admin_action_logs_on_target_type_and_target_id"
+  end
+
   create_table "blocks", force: :cascade do |t|
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
@@ -181,6 +194,18 @@ ActiveRecord::Schema.define(version: 20171116161857) do
     t.bigint "account_id", null: false
   end
 
+  create_table "invites", force: :cascade do |t|
+    t.bigint "user_id"
+    t.string "code", default: "", null: false
+    t.datetime "expires_at"
+    t.integer "max_uses"
+    t.integer "uses", default: 0, null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["code"], name: "index_invites_on_code", unique: true
+    t.index ["user_id"], name: "index_invites_on_user_id"
+  end
+
   create_table "list_accounts", force: :cascade do |t|
     t.bigint "list_id", null: false
     t.bigint "account_id", null: false
@@ -397,7 +422,7 @@ ActiveRecord::Schema.define(version: 20171116161857) do
     t.index ["account_id", "id"], name: "index_statuses_on_account_id_id"
     t.index ["conversation_id"], name: "index_statuses_on_conversation_id"
     t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
-    t.index ["reblog_of_id"], name: "index_statuses_on_reblog_of_id"
+    t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
     t.index ["uri"], name: "index_statuses_on_uri", unique: true
   end
 
@@ -415,7 +440,7 @@ ActiveRecord::Schema.define(version: 20171116161857) do
     t.datetime "updated_at", null: false
     t.boolean "hidden", default: false, null: false
     t.bigint "account_id"
-    t.index ["account_id"], name: "index_stream_entries_on_account_id"
+    t.index ["account_id", "activity_type", "id"], name: "index_stream_entries_on_account_id_and_activity_type_and_id"
     t.index ["activity_id", "activity_type"], name: "index_stream_entries_on_activity_id_and_activity_type"
   end
 
@@ -470,6 +495,7 @@ ActiveRecord::Schema.define(version: 20171116161857) do
     t.bigint "account_id", null: false
     t.boolean "disabled", default: false, null: false
     t.boolean "moderator", default: false, null: false
+    t.bigint "invite_id"
     t.index ["account_id"], name: "index_users_on_account_id"
     t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
     t.index ["email"], name: "index_users_on_email", unique: true
@@ -497,6 +523,8 @@ ActiveRecord::Schema.define(version: 20171116161857) do
   add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
   add_foreign_key "account_moderation_notes", "accounts"
   add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id"
+  add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify
+  add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade
   add_foreign_key "blocks", "accounts", column: "target_account_id", name: "fk_9571bfabc1", on_delete: :cascade
   add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
   add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
@@ -509,6 +537,7 @@ ActiveRecord::Schema.define(version: 20171116161857) do
   add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
   add_foreign_key "glitch_keyword_mutes", "accounts", on_delete: :cascade
   add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
+  add_foreign_key "invites", "users", on_delete: :cascade
   add_foreign_key "list_accounts", "accounts", on_delete: :cascade
   add_foreign_key "list_accounts", "follows", on_delete: :cascade
   add_foreign_key "list_accounts", "lists", on_delete: :cascade
@@ -542,5 +571,6 @@ ActiveRecord::Schema.define(version: 20171116161857) do
   add_foreign_key "stream_entries", "accounts", name: "fk_5659b17554", on_delete: :cascade
   add_foreign_key "subscriptions", "accounts", name: "fk_9847d1cbb5", on_delete: :cascade
   add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade
+  add_foreign_key "users", "invites", on_delete: :nullify
   add_foreign_key "web_settings", "users", name: "fk_11910667b2", on_delete: :cascade
 end
diff --git a/package.json b/package.json
index 159181030..5ee4e76a9 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
     "axios": "~0.16.2",
     "babel-core": "^6.25.0",
     "babel-loader": "^7.1.1",
-    "babel-plugin-lodash": "^3.2.11",
+    "babel-plugin-lodash": "^3.3.2",
     "babel-plugin-preval": "^1.6.1",
     "babel-plugin-react-intl": "^2.3.1",
     "babel-plugin-syntax-dynamic-import": "^6.18.0",
@@ -55,9 +55,9 @@
     "glob": "^7.1.1",
     "http-link-header": "^0.8.0",
     "immutable": "^3.8.2",
-    "intersection-observer": "^0.4.0",
+    "intersection-observer": "^0.5.0",
     "intl": "^1.2.5",
-    "intl-messageformat": "^2.1.0",
+    "intl-messageformat": "^2.2.0",
     "intl-relativeformat": "^2.1.0",
     "is-nan": "^1.2.1",
     "js-yaml": "^3.9.0",
@@ -65,22 +65,22 @@
     "mark-loader": "^0.1.6",
     "marky": "^1.2.0",
     "mkdirp": "^0.5.1",
-    "node-sass": "^4.5.2",
+    "node-sass": "^4.7.2",
     "npmlog": "^4.1.2",
     "object-assign": "^4.1.1",
     "object-fit-images": "^3.2.3",
     "offline-plugin": "^4.8.3",
     "path-complete-extname": "^0.1.0",
     "pg": "^6.4.0",
-    "postcss-loader": "^2.0.8",
+    "postcss-loader": "^2.0.9",
     "postcss-object-fit-images": "^1.1.2",
     "postcss-smart-import": "^0.7.5",
     "precss": "^2.0.0",
     "prop-types": "^15.5.10",
     "punycode": "^2.1.0",
     "rails-ujs": "^5.1.2",
-    "react": "^16.0.0",
-    "react-dom": "^16.0.0",
+    "react": "^16.2.0",
+    "react-dom": "^16.2.0",
     "react-hotkeys": "^0.10.0",
     "react-immutable-proptypes": "^2.1.0",
     "react-immutable-pure-component": "^1.1.1",
@@ -93,7 +93,7 @@
     "react-router-dom": "^4.1.1",
     "react-router-scroll-4": "^1.0.0-beta.1",
     "react-swipeable-views": "^0.12.3",
-    "react-textarea-autosize": "^5.0.7",
+    "react-textarea-autosize": "^5.2.1",
     "react-toggle": "^4.0.1",
     "redis": "^2.7.1",
     "redux": "^3.7.1",
@@ -104,23 +104,23 @@
     "resolve-url-loader": "^2.2.0",
     "rimraf": "^2.6.1",
     "sass-loader": "^6.0.6",
-    "stringz": "^0.2.2",
+    "stringz": "^0.3.0",
     "style-loader": "^0.19.0",
     "substring-trie": "^1.0.2",
     "throng": "^4.0.0",
     "tiny-queue": "^0.2.1",
     "uuid": "^3.1.0",
     "uws": "^8.14.0",
-    "webpack": "^3.8.1",
-    "webpack-bundle-analyzer": "^2.8.3",
+    "webpack": "^3.9.1",
+    "webpack-bundle-analyzer": "^2.9.1",
     "webpack-manifest-plugin": "^1.2.1",
-    "webpack-merge": "^4.1.0",
+    "webpack-merge": "^4.1.1",
     "websocket.js": "^0.1.12"
   },
   "devDependencies": {
     "babel-eslint": "^7.2.3",
-    "enzyme": "^3.0.0",
-    "enzyme-adapter-react-16": "^1.0.2",
+    "enzyme": "^3.2.0",
+    "enzyme-adapter-react-16": "^1.1.0",
     "eslint": "^3.19.0",
     "eslint-plugin-import": "^2.8.0",
     "eslint-plugin-jsx-a11y": "^4.0.0",
@@ -128,8 +128,8 @@
     "jest": "^21.2.1",
     "raf": "^3.4.0",
     "react-intl-translations-manager": "^5.0.0",
-    "react-test-renderer": "^16.0.0",
-    "webpack-dev-server": "^2.9.3",
+    "react-test-renderer": "^16.2.0",
+    "webpack-dev-server": "^2.9.5",
     "yargs": "^8.0.2"
   },
   "optionalDependencies": {
diff --git a/spec/fabricators/admin_action_log_fabricator.rb b/spec/fabricators/admin_action_log_fabricator.rb
new file mode 100644
index 000000000..2f44e953d
--- /dev/null
+++ b/spec/fabricators/admin_action_log_fabricator.rb
@@ -0,0 +1,5 @@
+Fabricator('Admin::ActionLog') do
+  account nil
+  action  "MyString"
+  target  nil
+end
diff --git a/spec/fabricators/invite_fabricator.rb b/spec/fabricators/invite_fabricator.rb
new file mode 100644
index 000000000..62b9b3904
--- /dev/null
+++ b/spec/fabricators/invite_fabricator.rb
@@ -0,0 +1,6 @@
+Fabricator(:invite) do
+  user
+  expires_at nil
+  max_uses   nil
+  uses       0
+end
diff --git a/spec/lib/activitypub/activity/delete_spec.rb b/spec/lib/activitypub/activity/delete_spec.rb
index 38254e31c..37b93ecf7 100644
--- a/spec/lib/activitypub/activity/delete_spec.rb
+++ b/spec/lib/activitypub/activity/delete_spec.rb
@@ -1,8 +1,8 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::Activity::Delete do
-  let(:sender)    { Fabricate(:account, domain: 'example.com') }
-  let(:status)    { Fabricate(:status, account: sender, uri: 'foobar') }
+  let(:sender) { Fabricate(:account, domain: 'example.com') }
+  let(:status) { Fabricate(:status, account: sender, uri: 'foobar') }
 
   let(:json) do
     {
@@ -30,13 +30,13 @@ RSpec.describe ActivityPub::Activity::Delete do
   context 'when the status has been reblogged' do
     describe '#perform' do
       subject { described_class.new(json, sender) }
-      let(:reblogger) { Fabricate(:account) }
-      let(:follower)   { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
+      let!(:reblogger) { Fabricate(:account) }
+      let!(:follower)  { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
+      let!(:reblog)    { Fabricate(:status, account: reblogger, reblog: status) }
 
       before do
         stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
         follower.follow!(reblogger)
-        Fabricate(:status, account: reblogger, reblog: status)
         subject.perform
       end
 
@@ -45,8 +45,7 @@ RSpec.describe ActivityPub::Activity::Delete do
       end
 
       it 'sends delete activity to followers of rebloggers' do
-        # one for Delete original post, and one for Undo reblog (normal delivery)
-        expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice
+        expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
       end
     end
   end
diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb
index ba96b6e7e..f87ef383a 100644
--- a/spec/lib/feed_manager_spec.rb
+++ b/spec/lib/feed_manager_spec.rb
@@ -164,6 +164,22 @@ RSpec.describe FeedManager do
 
         expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
       end
+
+      it 'returns true for a status with a tag that matches a muted keyword' do
+        Fabricate('Glitch::KeywordMute', account: alice, keyword: 'jorts')
+        status = Fabricate(:status, account: bob)
+	status.tags << Fabricate(:tag, name: 'jorts')
+
+        expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
+      end
+
+      it 'returns true for a status with a tag that matches an octothorpe-prefixed muted keyword' do
+        Fabricate('Glitch::KeywordMute', account: alice, keyword: '#jorts')
+        status = Fabricate(:status, account: bob)
+	status.tags << Fabricate(:tag, name: 'jorts')
+
+        expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
+      end
     end
 
     context 'for mentions feed' do
diff --git a/spec/lib/settings/extend_spec.rb b/spec/lib/settings/extend_spec.rb
new file mode 100644
index 000000000..83ced4230
--- /dev/null
+++ b/spec/lib/settings/extend_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Settings::Extend do
+  class User
+    include Settings::Extend
+  end
+
+  describe '#settings' do
+    it 'sets @settings as an instance of Settings::ScopedSettings' do
+      user = Fabricate(:user)
+      expect(user.settings).to be_kind_of Settings::ScopedSettings
+    end
+  end
+end
diff --git a/spec/models/admin/action_log_spec.rb b/spec/models/admin/action_log_spec.rb
new file mode 100644
index 000000000..59206a36b
--- /dev/null
+++ b/spec/models/admin/action_log_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Admin::ActionLog, type: :model do
+
+end
diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb
index 1e238e27c..95bf9561d 100644
--- a/spec/models/concerns/account_interactions_spec.rb
+++ b/spec/models/concerns/account_interactions_spec.rb
@@ -1,6 +1,561 @@
 require 'rails_helper'
 
 describe AccountInteractions do
+  let(:account)            { Fabricate(:account, username: 'account') }
+  let(:account_id)         { account.id }
+  let(:account_ids)        { [account_id] }
+  let(:target_account)     { Fabricate(:account, username: 'target') }
+  let(:target_account_id)  { target_account.id }
+  let(:target_account_ids) { [target_account_id] }
+
+  describe '.following_map' do
+    subject { Account.following_map(target_account_ids, account_id) }
+
+    context 'account with Follow' do
+      it 'returns { target_account_id => { reblogs: true } }' do
+        Fabricate(:follow, account: account, target_account: target_account)
+        is_expected.to eq(target_account_id => { reblogs: true })
+      end
+    end
+
+    context 'account with Follow but with reblogs disabled' do
+      it 'returns { target_account_id => { reblogs: false } }' do
+        Fabricate(:follow, account: account, target_account: target_account, show_reblogs: false)
+        is_expected.to eq(target_account_id => { reblogs: false })
+      end
+    end
+
+    context 'account without Follow' do
+      it 'returns {}' do
+        is_expected.to eq({})
+      end
+    end
+  end
+
+  describe '.followed_by_map' do
+    subject { Account.followed_by_map(target_account_ids, account_id) }
+
+    context 'account with Follow' do
+      it 'returns { target_account_id => true }' do
+        Fabricate(:follow, account: target_account, target_account: account)
+        is_expected.to eq(target_account_id => true)
+      end
+    end
+
+    context 'account without Follow' do
+      it 'returns {}' do
+        is_expected.to eq({})
+      end
+    end
+  end
+
+  describe '.blocking_map' do
+    subject { Account.blocking_map(target_account_ids, account_id) }
+
+    context 'account with Block' do
+      it 'returns { target_account_id => true }' do
+        Fabricate(:block, account: account, target_account: target_account)
+        is_expected.to eq(target_account_id => true)
+      end
+    end
+
+    context 'account without Block' do
+      it 'returns {}' do
+        is_expected.to eq({})
+      end
+    end
+  end
+
+  describe '.muting_map' do
+    subject { Account.muting_map(target_account_ids, account_id) }
+
+    context 'account with Mute' do
+      before do
+        Fabricate(:mute, target_account: target_account, account: account, hide_notifications: hide)
+      end
+
+      context 'if Mute#hide_notifications?' do
+        let(:hide) { true }
+
+        it 'returns { target_account_id => { notifications: true } }' do
+          is_expected.to eq(target_account_id => { notifications: true })
+        end
+      end
+
+      context 'unless Mute#hide_notifications?' do
+        let(:hide) { false }
+
+        it 'returns { target_account_id => { notifications: false } }' do
+          is_expected.to eq(target_account_id => { notifications: false })
+        end
+      end
+    end
+
+    context 'account without Mute' do
+      it 'returns {}' do
+        is_expected.to eq({})
+      end
+    end
+  end
+
+  describe '#follow!' do
+    it 'creates and returns Follow' do
+      expect do
+        expect(account.follow!(target_account)).to be_kind_of Follow
+      end.to change { account.following.count }.by 1
+    end
+  end
+
+  describe '#block' do
+    it 'creates and returns Block' do
+      expect do
+        expect(account.block!(target_account)).to be_kind_of Block
+      end.to change { account.block_relationships.count }.by 1
+    end
+  end
+
+  describe '#mute!' do
+    context 'Mute does not exist yet' do
+      context 'arg :notifications is nil' do
+        let(:arg_notifications) { nil }
+
+        it 'creates Mute, and returns nil' do
+          expect do
+            expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
+          end.to change { account.mute_relationships.count }.by 1
+        end
+      end
+
+      context 'arg :notifications is false' do
+        let(:arg_notifications) { false }
+
+        it 'creates Mute, and returns nil' do
+          expect do
+            expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
+          end.to change { account.mute_relationships.count }.by 1
+        end
+      end
+
+      context 'arg :notifications is true' do
+        let(:arg_notifications) { true }
+
+        it 'creates Mute, and returns nil' do
+          expect do
+            expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
+          end.to change { account.mute_relationships.count }.by 1
+        end
+      end
+    end
+
+    context 'Mute already exists' do
+      before do
+        account.mute_relationships << mute
+      end
+
+      let(:mute) do
+        Fabricate(:mute,
+                  account:            account,
+                  target_account:     target_account,
+                  hide_notifications: hide_notifications)
+      end
+
+      context 'mute.hide_notifications is true' do
+        let(:hide_notifications) { true }
+
+        context 'arg :notifications is nil' do
+          let(:arg_notifications) { nil }
+
+          it 'returns nil without updating mute.hide_notifications' do
+            expect do
+              expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
+              mute = account.mute_relationships.find_by(target_account: target_account)
+              expect(mute.hide_notifications?).to be true
+            end
+          end
+        end
+
+        context 'arg :notifications is false' do
+          let(:arg_notifications) { false }
+
+          it 'returns true, and updates mute.hide_notifications false' do
+            expect do
+              expect(account.mute!(target_account, notifications: arg_notifications)).to be true
+              mute = account.mute_relationships.find_by(target_account: target_account)
+              expect(mute.hide_notifications?).to be false
+            end
+          end
+        end
+
+        context 'arg :notifications is true' do
+          let(:arg_notifications) { true }
+
+          it 'returns nil without updating mute.hide_notifications' do
+            expect do
+              expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
+              mute = account.mute_relationships.find_by(target_account: target_account)
+              expect(mute.hide_notifications?).to be true
+            end
+          end
+        end
+      end
+
+      context 'mute.hide_notifications is false' do
+        let(:hide_notifications) { false }
+
+        context 'arg :notifications is nil' do
+          let(:arg_notifications) { nil }
+
+          it 'returns true, and updates mute.hide_notifications true' do
+            expect do
+              expect(account.mute!(target_account, notifications: arg_notifications)).to be true
+              mute = account.mute_relationships.find_by(target_account: target_account)
+              expect(mute.hide_notifications?).to be true
+            end
+          end
+        end
+
+        context 'arg :notifications is false' do
+          let(:arg_notifications) { false }
+
+          it 'returns nil without updating mute.hide_notifications' do
+            expect do
+              expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
+              mute = account.mute_relationships.find_by(target_account: target_account)
+              expect(mute.hide_notifications?).to be false
+            end
+          end
+        end
+
+        context 'arg :notifications is true' do
+          let(:arg_notifications) { true }
+
+          it 'returns true, and updates mute.hide_notifications true' do
+            expect do
+              expect(account.mute!(target_account, notifications: arg_notifications)).to be true
+              mute = account.mute_relationships.find_by(target_account: target_account)
+              expect(mute.hide_notifications?).to be true
+            end
+          end
+        end
+      end
+    end
+  end
+
+  describe '#mute_conversation!' do
+    let(:conversation) { Fabricate(:conversation) }
+
+    subject { account.mute_conversation!(conversation) }
+
+    it 'creates and returns ConversationMute' do
+      expect do
+        is_expected.to be_kind_of ConversationMute
+      end.to change { account.conversation_mutes.count }.by 1
+    end
+  end
+
+  describe '#block_domain!' do
+    let(:domain_block) { Fabricate(:domain_block) }
+
+    subject { account.block_domain!(domain_block) }
+
+    it 'creates and returns AccountDomainBlock' do
+      expect do
+        is_expected.to be_kind_of AccountDomainBlock
+      end.to change { account.domain_blocks.count }.by 1
+    end
+  end
+
+  describe '#unfollow!' do
+    subject { account.unfollow!(target_account) }
+
+    context 'following target_account' do
+      it 'returns destroyed Follow' do
+        account.active_relationships.create(target_account: target_account)
+        is_expected.to be_kind_of Follow
+        expect(subject).to be_destroyed
+      end
+    end
+
+    context 'not following target_account' do
+      it 'returns nil' do
+        is_expected.to be_nil
+      end
+    end
+  end
+
+  describe '#unblock!' do
+    subject { account.unblock!(target_account) }
+
+    context 'blocking target_account' do
+      it 'returns destroyed Block' do
+        account.block_relationships.create(target_account: target_account)
+        is_expected.to be_kind_of Block
+        expect(subject).to be_destroyed
+      end
+    end
+
+    context 'not blocking target_account' do
+      it 'returns nil' do
+        is_expected.to be_nil
+      end
+    end
+  end
+
+  describe '#unmute!' do
+    subject { account.unmute!(target_account) }
+
+    context 'muting target_account' do
+      it 'returns destroyed Mute' do
+        account.mute_relationships.create(target_account: target_account)
+        is_expected.to be_kind_of Mute
+        expect(subject).to be_destroyed
+      end
+    end
+
+    context 'not muting target_account' do
+      it 'returns nil' do
+        is_expected.to be_nil
+      end
+    end
+  end
+
+  describe '#unmute_conversation!' do
+    let(:conversation) { Fabricate(:conversation) }
+
+    subject { account.unmute_conversation!(conversation) }
+
+    context 'muting the conversation' do
+      it 'returns destroyed ConversationMute' do
+        account.conversation_mutes.create(conversation: conversation)
+        is_expected.to be_kind_of ConversationMute
+        expect(subject).to be_destroyed
+      end
+    end
+
+    context 'not muting the conversation' do
+      it 'returns nil' do
+        is_expected.to be nil
+      end
+    end
+  end
+
+  describe '#unblock_domain!' do
+    let(:domain) { 'example.com' }
+
+    subject { account.unblock_domain!(domain) }
+
+    context 'blocking the domain' do
+      it 'returns destroyed AccountDomainBlock' do
+        account_domain_block = Fabricate(:account_domain_block, domain: domain)
+        account.domain_blocks << account_domain_block
+        is_expected.to be_kind_of AccountDomainBlock
+        expect(subject).to be_destroyed
+      end
+    end
+
+    context 'unblocking the domain' do
+      it 'returns nil' do
+        is_expected.to be_nil
+      end
+    end
+  end
+
+  describe '#following?' do
+    subject { account.following?(target_account) }
+
+    context 'following target_account' do
+      it 'returns true' do
+        account.active_relationships.create(target_account: target_account)
+        is_expected.to be true
+      end
+    end
+
+    context 'not following target_account' do
+      it 'returns false' do
+        is_expected.to be false
+      end
+    end
+  end
+
+  describe '#blocking?' do
+    subject { account.blocking?(target_account) }
+
+    context 'blocking target_account' do
+      it 'returns true' do
+        account.block_relationships.create(target_account: target_account)
+        is_expected.to be true
+      end
+    end
+
+    context 'not blocking target_account' do
+      it 'returns false' do
+        is_expected.to be false
+      end
+    end
+  end
+
+  describe '#domain_blocking?' do
+    let(:domain)               { 'example.com' }
+
+    subject { account.domain_blocking?(domain) }
+
+    context 'blocking the domain' do
+      it' returns true' do
+        account_domain_block = Fabricate(:account_domain_block, domain: domain)
+        account.domain_blocks << account_domain_block
+        is_expected.to be true
+      end
+    end
+
+    context 'not blocking the domain' do
+      it 'returns false' do
+        is_expected.to be false
+      end
+    end
+  end
+
+  describe '#muting?' do
+    subject { account.muting?(target_account) }
+
+    context 'muting target_account' do
+      it 'returns true' do
+        mute = Fabricate(:mute, account: account, target_account: target_account)
+        account.mute_relationships << mute
+        is_expected.to be true
+      end
+    end
+
+    context 'not muting target_account' do
+      it 'returns false' do
+        is_expected.to be false
+      end
+    end
+  end
+
+  describe '#muting_conversation?' do
+    let(:conversation) { Fabricate(:conversation) }
+
+    subject { account.muting_conversation?(conversation) }
+
+    context 'muting the conversation' do
+      it 'returns true' do
+        account.conversation_mutes.create(conversation: conversation)
+        is_expected.to be true
+      end
+    end
+
+    context 'not muting the conversation' do
+      it 'returns false' do
+        is_expected.to be false
+      end
+    end
+  end
+
+  describe '#muting_notifications?' do
+    before do
+      mute = Fabricate(:mute, target_account: target_account, account: account, hide_notifications: hide)
+      account.mute_relationships << mute
+    end
+
+    subject { account.muting_notifications?(target_account) }
+
+    context 'muting notifications of target_account' do
+      let(:hide) { true }
+
+      it 'returns true' do
+        is_expected.to be true
+      end
+    end
+
+    context 'not muting notifications of target_account' do
+      let(:hide) { false }
+
+      it 'returns false' do
+        is_expected.to be false
+      end
+    end
+  end
+
+  describe '#requested?' do
+    subject { account.requested?(target_account) }
+
+    context 'requested by target_account' do
+      it 'returns true' do
+        Fabricate(:follow_request, account: account, target_account: target_account)
+        is_expected.to be true
+      end
+    end
+
+    context 'not requested by target_account' do
+      it 'returns false' do
+        is_expected.to be false
+      end
+    end
+  end
+
+  describe '#favourited?' do
+    let(:status) { Fabricate(:status, account: account, favourites: favourites) }
+
+    subject { account.favourited?(status) }
+
+    context 'favorited' do
+      let(:favourites) { [Fabricate(:favourite, account: account)] }
+
+      it 'returns true' do
+        is_expected.to be true
+      end
+    end
+
+    context 'not favorited' do
+      let(:favourites) { [] }
+
+      it 'returns false' do
+        is_expected.to be false
+      end
+    end
+  end
+
+  describe '#reblogged?' do
+    let(:status) { Fabricate(:status, account: account, reblogs: reblogs) }
+
+    subject { account.reblogged?(status) }
+
+    context 'reblogged' do
+      let(:reblogs) { [Fabricate(:status, account: account)] }
+
+      it 'returns true' do
+        is_expected.to be true
+      end
+    end
+
+    context 'not reblogged' do
+      let(:reblogs) { [] }
+
+      it 'returns false' do
+        is_expected.to be false
+      end
+    end
+  end
+
+  describe '#pinned?' do
+    let(:status) { Fabricate(:status, account: account) }
+
+    subject { account.pinned?(status) }
+
+    context 'pinned' do
+      it 'returns true' do
+        Fabricate(:status_pin, account: account, status: status)
+        is_expected.to be true
+      end
+    end
+
+    context 'not pinned' do
+      it 'returns false' do
+        is_expected.to be false
+      end
+    end
+  end
+
   describe 'muting an account' do
     let(:me) { Fabricate(:account, username: 'Me') }
     let(:you) { Fabricate(:account, username: 'You') }
@@ -72,4 +627,41 @@ describe AccountInteractions do
       end
     end
   end
+
+  describe 'ignoring reblogs from an account' do
+    before do
+      @me = Fabricate(:account, username: 'Me')
+      @you = Fabricate(:account, username: 'You')
+    end
+
+    context 'with the reblogs option unspecified' do
+      before do
+        @me.follow!(@you)
+      end
+
+      it 'defaults to showing reblogs' do
+        expect(@me.muting_reblogs?(@you)).to be(false)
+      end
+    end
+
+    context 'with the reblogs option set to false' do
+      before do
+        @me.follow!(@you, reblogs: false)
+      end
+
+      it 'does mute reblogs' do
+        expect(@me.muting_reblogs?(@you)).to be(true)
+      end
+    end
+
+    context 'with the reblogs option set to true' do
+      before do
+        @me.follow!(@you, reblogs: true)
+      end
+
+      it 'does not mute reblogs' do
+        expect(@me.muting_reblogs?(@you)).to be(false)
+      end
+    end
+  end
 end
diff --git a/spec/models/concerns/remotable_spec.rb b/spec/models/concerns/remotable_spec.rb
new file mode 100644
index 000000000..0b2dad23f
--- /dev/null
+++ b/spec/models/concerns/remotable_spec.rb
@@ -0,0 +1,205 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Remotable do
+  class Foo
+    def initialize
+      @attrs = {}
+    end
+
+    def [](arg)
+      @attrs[arg]
+    end
+
+    def []=(arg1, arg2)
+      @attrs[arg1] = arg2
+    end
+
+    def hoge=(arg); end
+
+    def hoge_file_name=(arg); end
+
+    def has_attribute?(arg); end
+
+    def self.attachment_definitions
+      { hoge: nil }
+    end
+  end
+
+  context 'Remotable module is included' do
+    before do
+      class Foo; include Remotable; end
+    end
+
+    let(:attribute_name) { "#{hoge}_remote_url".to_sym }
+    let(:code)           { 200 }
+    let(:file)           { 'filename="foo.txt"' }
+    let(:foo)            { Foo.new }
+    let(:headers)        { { 'content-disposition' => file } }
+    let(:hoge)           { :hoge }
+    let(:url)            { 'https://google.com' }
+
+    let(:request) do
+      stub_request(:get, url)
+        .to_return(status: code, headers: headers)
+    end
+
+    it 'defines a method #hoge_remote_url=' do
+      expect(foo).to respond_to(:hoge_remote_url=)
+    end
+
+    it 'defines a method #reset_hoge!' do
+      expect(foo).to respond_to(:reset_hoge!)
+    end
+
+    describe '#hoge_remote_url' do
+      before do
+        request
+      end
+
+      it 'always returns arg' do
+        [nil, '', [], {}].each do |arg|
+          expect(foo.hoge_remote_url = arg).to be arg
+        end
+      end
+
+      context 'Addressable::URI::InvalidURIError raised' do
+        it 'makes no request' do
+          allow(Addressable::URI).to receive_message_chain(:parse, :normalize)
+            .with(url).with(no_args).and_raise(Addressable::URI::InvalidURIError)
+
+          foo.hoge_remote_url = url
+          expect(request).not_to have_been_requested
+        end
+      end
+
+      context 'scheme is neither http nor https' do
+        let(:url) { 'ftp://google.com' }
+
+        it 'makes no request' do
+          foo.hoge_remote_url = url
+          expect(request).not_to have_been_requested
+        end
+      end
+
+      context 'parsed_url.host is empty' do
+        it 'makes no request' do
+          parsed_url = double(scheme: 'https', host: double(empty?: true))
+          allow(Addressable::URI).to receive_message_chain(:parse, :normalize)
+            .with(url).with(no_args).and_return(parsed_url)
+
+          foo.hoge_remote_url = url
+          expect(request).not_to have_been_requested
+        end
+      end
+
+      context 'foo[attribute_name] == url' do
+        it 'makes no request' do
+          allow(foo).to receive(:[]).with(attribute_name).and_return(url)
+
+          foo.hoge_remote_url = url
+          expect(request).not_to have_been_requested
+        end
+      end
+
+      context "scheme is https, parsed_url.host isn't empty, and foo[attribute_name] != url" do
+        it 'makes a request' do
+          foo.hoge_remote_url = url
+          expect(request).to have_been_requested
+        end
+
+        context 'response.code != 200' do
+          let(:code) { 500 }
+
+          it 'calls not send' do
+            expect(foo).not_to receive(:send).with("#{hoge}=", any_args)
+            expect(foo).not_to receive(:send).with("#{hoge}_file_name=", any_args)
+            foo.hoge_remote_url = url
+          end
+        end
+
+        context 'response.code == 200' do
+          let(:code) { 200 }
+
+          context 'response contains headers["content-disposition"]' do
+            let(:file)      { 'filename="foo.txt"' }
+            let(:headers)   { { 'content-disposition' => file } }
+
+            it 'calls send' do
+              string_io = StringIO.new('')
+              extname   = '.txt'
+              basename  = '0123456789abcdef'
+
+              allow(SecureRandom).to receive(:hex).and_return(basename)
+              allow(StringIO).to receive(:new).with(anything).and_return(string_io)
+
+              expect(foo).to receive(:send).with("#{hoge}=", string_io)
+              expect(foo).to receive(:send).with("#{hoge}_file_name=", basename + extname)
+              foo.hoge_remote_url = url
+            end
+          end
+
+          context 'if has_attribute?' do
+            it 'calls foo[attribute_name] = url' do
+              allow(foo).to receive(:has_attribute?).with(attribute_name).and_return(true)
+              expect(foo).to receive('[]=').with(attribute_name, url)
+              foo.hoge_remote_url = url
+            end
+          end
+
+          context 'unless has_attribute?' do
+            it 'calls not foo[attribute_name] = url' do
+              allow(foo).to receive(:has_attribute?)
+                .with(attribute_name).and_return(false)
+              expect(foo).not_to receive('[]=').with(attribute_name, url)
+              foo.hoge_remote_url = url
+            end
+          end
+        end
+
+        context 'an error raised during the request' do
+          let(:request) { stub_request(:get, url).to_raise(error_class) }
+
+          error_classes = [
+            HTTP::TimeoutError,
+            HTTP::ConnectionError,
+            OpenSSL::SSL::SSLError,
+            Paperclip::Errors::NotIdentifiedByImageMagickError,
+            Addressable::URI::InvalidURIError,
+          ]
+
+          error_classes.each do |error_class|
+            let(:error_class) { error_class }
+
+            it 'calls Rails.logger.debug' do
+              expect(Rails.logger).to receive(:debug).with(/^Error fetching remote #{hoge}: /)
+              foo.hoge_remote_url = url
+            end
+          end
+        end
+      end
+    end
+
+    describe '#reset_hoge!' do
+      context 'if url.blank?' do
+        it 'returns nil, without clearing foo[attribute_name] and calling #hoge_remote_url=' do
+          url = nil
+          expect(foo).not_to receive(:send).with(:hoge_remote_url=, url)
+          foo[attribute_name] = url
+          expect(foo.reset_hoge!).to be_nil
+          expect(foo[attribute_name]).to be_nil
+        end
+      end
+
+      context 'unless url.blank?' do
+        it 'clears foo[attribute_name] and calls #hoge_remote_url=' do
+          foo[attribute_name] = url
+          expect(foo).to receive(:send).with(:hoge_remote_url=, url)
+          foo.reset_hoge!
+          expect(foo[attribute_name]).to be ''
+        end
+      end
+    end
+  end
+end
diff --git a/spec/models/concerns/streamable_spec.rb b/spec/models/concerns/streamable_spec.rb
new file mode 100644
index 000000000..b5f2d5192
--- /dev/null
+++ b/spec/models/concerns/streamable_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Streamable do
+  class Parent
+    def title; end
+
+    def target; end
+
+    def thread; end
+
+    def self.has_one(*); end
+
+    def self.after_create; end
+  end
+
+  class Child < Parent
+    include Streamable
+  end
+
+  child = Child.new
+
+  describe '#title' do
+    it 'calls Parent#title' do
+      expect_any_instance_of(Parent).to receive(:title)
+      child.title
+    end
+  end
+
+  describe '#content' do
+    it 'calls #title' do
+      expect_any_instance_of(Parent).to receive(:title)
+      child.content
+    end
+  end
+
+  describe '#target' do
+    it 'calls Parent#target' do
+      expect_any_instance_of(Parent).to receive(:target)
+      child.target
+    end
+  end
+
+  describe '#object_type' do
+    it 'returns :activity' do
+      expect(child.object_type).to eq :activity
+    end
+  end
+
+  describe '#thread' do
+    it 'calls Parent#thread' do
+      expect_any_instance_of(Parent).to receive(:thread)
+      child.thread
+    end
+  end
+
+  describe '#hidden?' do
+    it 'returns false' do
+      expect(child.hidden?).to be false
+    end
+  end
+end
diff --git a/spec/models/glitch/keyword_mute_spec.rb b/spec/models/glitch/keyword_mute_spec.rb
index 9685c6493..0ffc7b18f 100644
--- a/spec/models/glitch/keyword_mute_spec.rb
+++ b/spec/models/glitch/keyword_mute_spec.rb
@@ -4,8 +4,8 @@ RSpec.describe Glitch::KeywordMute, type: :model do
   let(:alice) { Fabricate(:account, username: 'alice').tap(&:save!) }
   let(:bob) { Fabricate(:account, username: 'bob').tap(&:save!) }
 
-  describe '.matcher_for' do
-    let(:matcher) { Glitch::KeywordMute.matcher_for(alice) }
+  describe '.text_matcher_for' do
+    let(:matcher) { Glitch::KeywordMute.text_matcher_for(alice.id) }
 
     describe 'with no mutes' do
       before do
@@ -13,7 +13,7 @@ RSpec.describe Glitch::KeywordMute, type: :model do
       end
 
       it 'does not match' do
-        expect(matcher =~ 'This is a hot take').to be_falsy
+        expect(matcher.matches?('This is a hot take')).to be_falsy
       end
     end
 
@@ -21,75 +21,136 @@ RSpec.describe Glitch::KeywordMute, type: :model do
       it 'does not match keywords set by a different account' do
         Glitch::KeywordMute.create!(account: bob, keyword: 'take')
 
-        expect(matcher =~ 'This is a hot take').to be_falsy
+        expect(matcher.matches?('This is a hot take')).to be_falsy
       end
 
       it 'does not match if no keywords match the status text' do
         Glitch::KeywordMute.create!(account: alice, keyword: 'cold')
 
-        expect(matcher =~ 'This is a hot take').to be_falsy
+        expect(matcher.matches?('This is a hot take')).to be_falsy
       end
 
       it 'considers word boundaries when matching' do
         Glitch::KeywordMute.create!(account: alice, keyword: 'bob', whole_word: true)
 
-        expect(matcher =~ 'bobcats').to be_falsy
+        expect(matcher.matches?('bobcats')).to be_falsy
       end
 
       it 'matches substrings if whole_word is false' do
         Glitch::KeywordMute.create!(account: alice, keyword: 'take', whole_word: false)
 
-        expect(matcher =~ 'This is a shiitake mushroom').to be_truthy
+        expect(matcher.matches?('This is a shiitake mushroom')).to be_truthy
       end
 
       it 'matches keywords at the beginning of the text' do
         Glitch::KeywordMute.create!(account: alice, keyword: 'take')
 
-        expect(matcher =~ 'Take this').to be_truthy
+        expect(matcher.matches?('Take this')).to be_truthy
       end
 
       it 'matches keywords at the end of the text' do
         Glitch::KeywordMute.create!(account: alice, keyword: 'take')
 
-        expect(matcher =~ 'This is a hot take').to be_truthy
+        expect(matcher.matches?('This is a hot take')).to be_truthy
       end
 
       it 'matches if at least one keyword case-insensitively matches the text' do
         Glitch::KeywordMute.create!(account: alice, keyword: 'hot')
 
-        expect(matcher =~ 'This is a HOT take').to be_truthy
+        expect(matcher.matches?('This is a HOT take')).to be_truthy
       end
 
       it 'maintains case-insensitivity when combining keywords into a single matcher' do
         Glitch::KeywordMute.create!(account: alice, keyword: 'hot')
         Glitch::KeywordMute.create!(account: alice, keyword: 'cold')
 
-        expect(matcher =~ 'This is a HOT take').to be_truthy
+        expect(matcher.matches?('This is a HOT take')).to be_truthy
       end
 
       it 'matches keywords surrounded by non-alphanumeric ornamentation' do
         Glitch::KeywordMute.create!(account: alice, keyword: 'hot')
 
-        expect(matcher =~ '(hot take)').to be_truthy
+        expect(matcher.matches?('(hot take)')).to be_truthy
       end
 
       it 'escapes metacharacters in keywords' do
         Glitch::KeywordMute.create!(account: alice, keyword: '(hot take)')
 
-        expect(matcher =~ '(hot take)').to be_truthy
+        expect(matcher.matches?('(hot take)')).to be_truthy
       end
 
       it 'uses case-folding rules appropriate for more than just English' do
         Glitch::KeywordMute.create!(account: alice, keyword: 'großeltern')
 
-        expect(matcher =~ 'besuch der grosseltern').to be_truthy
+        expect(matcher.matches?('besuch der grosseltern')).to be_truthy
       end
 
       it 'matches keywords that are composed of multiple words' do
         Glitch::KeywordMute.create!(account: alice, keyword: 'a shiitake')
 
-        expect(matcher =~ 'This is a shiitake').to be_truthy
-        expect(matcher =~ 'This is shiitake').to_not be_truthy
+        expect(matcher.matches?('This is a shiitake')).to be_truthy
+        expect(matcher.matches?('This is shiitake')).to_not be_truthy
+      end
+    end
+  end
+
+  describe '.tag_matcher_for' do
+    let(:matcher) { Glitch::KeywordMute.tag_matcher_for(alice.id) }
+    let(:status) { Fabricate(:status) }
+
+    describe 'with no mutes' do
+      before do
+        Glitch::KeywordMute.delete_all
+      end
+
+      it 'does not match' do
+        status.tags << Fabricate(:tag, name: 'xyzzy')
+
+        expect(matcher.matches?(status.tags)).to be false
+      end
+    end
+
+    describe 'with mutes' do
+      it 'does not match keywords set by a different account' do
+        status.tags << Fabricate(:tag, name: 'xyzzy')
+        Glitch::KeywordMute.create!(account: bob, keyword: 'take')
+
+        expect(matcher.matches?(status.tags)).to be false
+      end
+
+      it 'matches #xyzzy when given the mute "#xyzzy"' do
+        status.tags << Fabricate(:tag, name: 'xyzzy')
+        Glitch::KeywordMute.create!(account: alice, keyword: '#xyzzy')
+
+        expect(matcher.matches?(status.tags)).to be true
+      end
+
+      it 'matches #thingiverse when given the non-whole-word mute "#thing"' do
+        status.tags << Fabricate(:tag, name: 'thingiverse')
+        Glitch::KeywordMute.create!(account: alice, keyword: '#thing', whole_word: false)
+
+        expect(matcher.matches?(status.tags)).to be true
+      end
+
+      it 'matches #hashtag when given the mute "##hashtag""' do
+        status.tags << Fabricate(:tag, name: 'hashtag')
+        Glitch::KeywordMute.create!(account: alice, keyword: '##hashtag')
+
+        expect(matcher.matches?(status.tags)).to be true
+      end
+
+      it 'matches #oatmeal when given the non-whole-word mute "oat"' do
+        status.tags << Fabricate(:tag, name: 'oatmeal')
+        Glitch::KeywordMute.create!(account: alice, keyword: 'oat', whole_word: false)
+
+        expect(matcher.matches?(status.tags)).to be true
+      end
+
+      it 'does not match #oatmeal when given the mute "#oat"' do
+        status.tags << Fabricate(:tag, name: 'oatmeal')
+        Glitch::KeywordMute.create!(account: alice, keyword: 'oat')
+
+        expect(matcher.matches?(status.tags)).to be false
       end
     end
   end
diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb
new file mode 100644
index 000000000..0ba1dccb3
--- /dev/null
+++ b/spec/models/invite_spec.rb
@@ -0,0 +1,30 @@
+require 'rails_helper'
+
+RSpec.describe Invite, type: :model do
+  describe '#valid_for_use?' do
+    it 'returns true when there are no limitations' do
+      invite = Invite.new(max_uses: nil, expires_at: nil)
+      expect(invite.valid_for_use?).to be true
+    end
+
+    it 'returns true when not expired' do
+      invite = Invite.new(max_uses: nil, expires_at: 1.hour.from_now)
+      expect(invite.valid_for_use?).to be true
+    end
+
+    it 'returns false when expired' do
+      invite = Invite.new(max_uses: nil, expires_at: 1.hour.ago)
+      expect(invite.valid_for_use?).to be false
+    end
+
+    it 'returns true when uses still available' do
+      invite = Invite.new(max_uses: 250, uses: 249, expires_at: nil)
+      expect(invite.valid_for_use?).to be true
+    end
+
+    it 'returns false when maximum uses reached' do
+      invite = Invite.new(max_uses: 250, uses: 250, expires_at: nil)
+      expect(invite.valid_for_use?).to be false
+    end
+  end
+end
diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb
index 763b1523f..8444c8f63 100644
--- a/spec/models/notification_spec.rb
+++ b/spec/models/notification_spec.rb
@@ -6,23 +6,18 @@ RSpec.describe Notification, type: :model do
   end
 
   describe '#target_status' do
-    before do
-      allow(notification).to receive(:type).and_return(type)
-      allow(notification).to receive(:activity).and_return(activity)
-    end
-
-    let(:notification) { Fabricate(:notification) }
-    let(:status)       { instance_double('Status') }
-    let(:favourite)    { instance_double('Favourite') }
-    let(:mention)      { instance_double('Mention') }
+    let(:notification) { Fabricate(:notification, activity_type: type, activity: activity) }
+    let(:status)       { Fabricate(:status) }
+    let(:reblog)       { Fabricate(:status, reblog: status) }
+    let(:favourite)    { Fabricate(:favourite, status: status) }
+    let(:mention)      { Fabricate(:mention, status: status) }
 
     context 'type is :reblog' do
       let(:type)     { :reblog }
-      let(:activity) { status }
+      let(:activity) { reblog }
 
-      it 'calls activity.reblog' do
-        expect(activity).to receive(:reblog)
-        notification.target_status
+      it 'returns status' do
+        expect(notification.target_status).to eq status
       end
     end
 
@@ -30,9 +25,8 @@ RSpec.describe Notification, type: :model do
       let(:type)     { :favourite }
       let(:activity) { favourite }
 
-      it 'calls activity.status' do
-        expect(activity).to receive(:status)
-        notification.target_status
+      it 'returns status' do
+        expect(notification.target_status).to eq status
       end
     end
 
@@ -40,9 +34,8 @@ RSpec.describe Notification, type: :model do
       let(:type)     { :mention }
       let(:activity) { mention }
 
-      it 'calls activity.status' do
-        expect(activity).to receive(:status)
-        notification.target_status
+      it 'returns status' do
+        expect(notification.target_status).to eq status
       end
     end
   end
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index 89ad3adcf..c6701018e 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -83,8 +83,31 @@ RSpec.describe Status, type: :model do
   end
 
   describe '#title' do
-    it 'is a shorter version of the content' do
-      expect(subject.title).to be_a String
+    # rubocop:disable Style/InterpolationCheck
+
+    let(:account) { subject.account }
+
+    context 'if destroyed?' do
+      it 'returns "#{account.acct} deleted status"' do
+        subject.destroy!
+        expect(subject.title).to eq "#{account.acct} deleted status"
+      end
+    end
+
+    context 'unless destroyed?' do
+      context 'if reblog?' do
+        it 'returns "#{account.acct} shared a status by #{reblog.account.acct}"' do
+          reblog = subject.reblog = other
+          expect(subject.title).to eq "#{account.acct} shared a status by #{reblog.account.acct}"
+        end
+      end
+
+      context 'unless reblog?' do
+        it 'returns "New status by #{account.acct}"' do
+          subject.reblog = nil
+          expect(subject.title).to eq "New status by #{account.acct}"
+        end
+      end
     end
   end
 
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 77a12c26d..5ed7ed88b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -273,4 +273,47 @@ RSpec.describe User, type: :model do
       expect(user.token_for_app(app)).to be_nil
     end
   end
+
+  describe '#role' do
+    it 'returns admin for admin' do
+      user = User.new(admin: true)
+      expect(user.role).to eq 'admin'
+    end
+
+    it 'returns moderator for moderator' do
+      user = User.new(moderator: true)
+      expect(user.role).to eq 'moderator'
+    end
+
+    it 'returns user otherwise' do
+      user = User.new
+      expect(user.role).to eq 'user'
+    end
+  end
+
+  describe '#role?' do
+    it 'returns false when invalid role requested' do
+      user = User.new(admin: true)
+      expect(user.role?('disabled')).to be false
+    end
+
+    it 'returns true when exact role match' do
+      user  = User.new
+      mod   = User.new(moderator: true)
+      admin = User.new(admin: true)
+
+      expect(user.role?('user')).to be true
+      expect(mod.role?('moderator')).to be true
+      expect(admin.role?('admin')).to be true
+    end
+
+    it 'returns true when role higher than needed' do
+      mod   = User.new(moderator: true)
+      admin = User.new(admin: true)
+
+      expect(mod.role?('user')).to be true
+      expect(admin.role?('user')).to be true
+      expect(admin.role?('moderator')).to be true
+    end
+  end
 end
diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb
new file mode 100644
index 000000000..f8b048d38
--- /dev/null
+++ b/spec/presenters/account_relationships_presenter_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AccountRelationshipsPresenter do
+  describe '.initialize' do
+    before do
+      allow(Account).to receive(:following_map).with(account_ids, current_account_id).and_return(default_map)
+      allow(Account).to receive(:followed_by_map).with(account_ids, current_account_id).and_return(default_map)
+      allow(Account).to receive(:blocking_map).with(account_ids, current_account_id).and_return(default_map)
+      allow(Account).to receive(:muting_map).with(account_ids, current_account_id).and_return(default_map)
+      allow(Account).to receive(:requested_map).with(account_ids, current_account_id).and_return(default_map)
+      allow(Account).to receive(:domain_blocking_map).with(account_ids, current_account_id).and_return(default_map)
+    end
+
+    let(:presenter)          { AccountRelationshipsPresenter.new(account_ids, current_account_id, options) }
+    let(:current_account_id) { Fabricate(:account).id }
+    let(:account_ids)        { [Fabricate(:account).id] }
+    let(:default_map)        { { 1 => true } }
+
+    context 'options are not set' do
+      let(:options) { {} }
+
+      it 'sets default maps' do
+        expect(presenter.following).to       eq default_map
+        expect(presenter.followed_by).to     eq default_map
+        expect(presenter.blocking).to        eq default_map
+        expect(presenter.muting).to          eq default_map
+        expect(presenter.requested).to       eq default_map
+        expect(presenter.domain_blocking).to eq default_map
+      end
+    end
+
+    context 'options[:following_map] is set' do
+      let(:options) { { following_map: { 2 => true } } }
+
+      it 'sets @following merged with default_map and options[:following_map]' do
+        expect(presenter.following).to eq default_map.merge(options[:following_map])
+      end
+    end
+
+    context 'options[:followed_by_map] is set' do
+      let(:options) { { followed_by_map: { 3 => true } } }
+
+      it 'sets @followed_by merged with default_map and options[:followed_by_map]' do
+        expect(presenter.followed_by).to eq default_map.merge(options[:followed_by_map])
+      end
+    end
+
+    context 'options[:blocking_map] is set' do
+      let(:options) { { blocking_map: { 4 => true } } }
+
+      it 'sets @blocking merged with default_map and options[:blocking_map]' do
+        expect(presenter.blocking).to eq default_map.merge(options[:blocking_map])
+      end
+    end
+
+    context 'options[:muting_map] is set' do
+      let(:options) { { muting_map: { 5 => true } } }
+
+      it 'sets @muting merged with default_map and options[:muting_map]' do
+        expect(presenter.muting).to eq default_map.merge(options[:muting_map])
+      end
+    end
+
+    context 'options[:requested_map] is set' do
+      let(:options) { { requested_map: { 6 => true } } }
+
+      it 'sets @requested merged with default_map and options[:requested_map]' do
+        expect(presenter.requested).to eq default_map.merge(options[:requested_map])
+      end
+    end
+
+    context 'options[:domain_blocking_map] is set' do
+      let(:options) { { domain_blocking_map: { 7 => true } } }
+
+      it 'sets @domain_blocking merged with default_map and options[:domain_blocking_map]' do
+        expect(presenter.domain_blocking).to eq default_map.merge(options[:domain_blocking_map])
+      end
+    end
+  end
+end
diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb
index 51f3fe3a1..ad26abc5b 100644
--- a/spec/services/activitypub/fetch_remote_status_service_spec.rb
+++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb
@@ -1,6 +1,8 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::FetchRemoteStatusService do
+  include ActionView::Helpers::TextHelper
+
   let(:sender) { Fabricate(:account) }
   let(:recipient) { Fabricate(:account) }
   let(:valid_domain) { Rails.configuration.x.local_domain }
@@ -19,6 +21,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
 
   describe '#call' do
     before do
+      stub_request(:head, 'https://example.com/watch?v=12345').to_return(status: 404, body: '')
       subject.call(object[:id], prefetched_body: Oj.dump(object))
     end
 
@@ -32,5 +35,38 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
         expect(status.text).to eq 'Lorem ipsum'
       end
     end
+
+    context 'with Video object' do
+      let(:object) do
+        {
+          '@context': 'https://www.w3.org/ns/activitystreams',
+          id: "https://#{valid_domain}/@foo/1234",
+          type: 'Video',
+          name: 'Nyan Cat 10 hours remix',
+          attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
+          url: [
+            {
+              type: 'Link',
+              mimeType: 'application/x-bittorrent',
+              href: 'https://example.com/12345.torrent',
+            },
+
+            {
+              type: 'Link',
+              mimeType: 'text/html',
+              href: 'https://example.com/watch?v=12345',
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.url).to eq 'https://example.com/watch?v=12345'
+        expect(strip_tags(status.text)).to eq 'Nyan Cat 10 hours remix https://example.com/watch?v=12345'
+      end
+    end
   end
 end
diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb
index a8ebc16b8..bb7601e76 100644
--- a/spec/services/notify_service_spec.rb
+++ b/spec/services/notify_service_spec.rb
@@ -101,6 +101,26 @@ RSpec.describe NotifyService do
     end
   end
 
+  describe 'reblogs' do
+    let(:status)   { Fabricate(:status, account: Fabricate(:account)) }
+    let(:activity) { Fabricate(:status, account: sender, reblog: status) }
+
+    it 'shows reblogs by default' do
+      recipient.follow!(sender)
+      is_expected.to change(Notification, :count)
+    end
+
+    it 'shows reblogs when explicitly enabled' do
+      recipient.follow!(sender, reblogs: true)
+      is_expected.to change(Notification, :count)
+    end
+
+    it 'hides reblogs when disabled' do
+      recipient.follow!(sender, reblogs: false)
+      is_expected.to_not change(Notification, :count)
+    end
+  end
+
   context do
     let(:asshole)  { Fabricate(:account, username: 'asshole') }
     let(:reply_to) { Fabricate(:status, account: asshole) }
diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb
index 09f8fa45b..19a8678f0 100644
--- a/spec/services/process_mentions_service_spec.rb
+++ b/spec/services/process_mentions_service_spec.rb
@@ -41,4 +41,25 @@ RSpec.describe ProcessMentionsService do
       expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
     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)
+      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
diff --git a/streaming/index.js b/streaming/index.js
index 42df63031..3048802e3 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -515,7 +515,7 @@ const startWorker = (workerId) => {
     });
   }, 30000);
 
-  server.listen(process.env.PORT || 4000, () => {
+  server.listen(process.env.PORT || 4000, process.env.BIND || '0.0.0.0', () => {
     log.info(`Worker ${workerId} now listening on ${server.address().address}:${server.address().port}`);
   });
 
diff --git a/yarn.lock b/yarn.lock
index 0805e8af3..cc5ccf720 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,9 +2,62 @@
 # yarn lockfile v1
 
 
-"@types/node@^6.0.46":
-  version "6.0.90"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.90.tgz#0ed74833fa1b73dcdb9409dcb1c97ec0a8b13b02"
+"@babel/code-frame@7.0.0-beta.31":
+  version "7.0.0-beta.31"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.31.tgz#473d021ecc573a2cce1c07d5b509d5215f46ba35"
+  dependencies:
+    chalk "^2.0.0"
+    esutils "^2.0.2"
+    js-tokens "^3.0.0"
+
+"@babel/helper-function-name@7.0.0-beta.31":
+  version "7.0.0-beta.31"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.31.tgz#afe63ad799209989348b1109b44feb66aa245f57"
+  dependencies:
+    "@babel/helper-get-function-arity" "7.0.0-beta.31"
+    "@babel/template" "7.0.0-beta.31"
+    "@babel/traverse" "7.0.0-beta.31"
+    "@babel/types" "7.0.0-beta.31"
+
+"@babel/helper-get-function-arity@7.0.0-beta.31":
+  version "7.0.0-beta.31"
+  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.31.tgz#1176d79252741218e0aec872ada07efb2b37a493"
+  dependencies:
+    "@babel/types" "7.0.0-beta.31"
+
+"@babel/template@7.0.0-beta.31":
+  version "7.0.0-beta.31"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.31.tgz#577bb29389f6c497c3e7d014617e7d6713f68bda"
+  dependencies:
+    "@babel/code-frame" "7.0.0-beta.31"
+    "@babel/types" "7.0.0-beta.31"
+    babylon "7.0.0-beta.31"
+    lodash "^4.2.0"
+
+"@babel/traverse@7.0.0-beta.31":
+  version "7.0.0-beta.31"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.31.tgz#db399499ad74aefda014f0c10321ab255134b1df"
+  dependencies:
+    "@babel/code-frame" "7.0.0-beta.31"
+    "@babel/helper-function-name" "7.0.0-beta.31"
+    "@babel/types" "7.0.0-beta.31"
+    babylon "7.0.0-beta.31"
+    debug "^3.0.1"
+    globals "^10.0.0"
+    invariant "^2.2.0"
+    lodash "^4.2.0"
+
+"@babel/types@7.0.0-beta.31":
+  version "7.0.0-beta.31"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.31.tgz#42c9c86784f674c173fb21882ca9643334029de4"
+  dependencies:
+    esutils "^2.0.2"
+    lodash "^4.2.0"
+    to-fast-properties "^2.0.0"
+
+"@types/node@*":
+  version "8.0.53"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.53.tgz#396b35af826fa66aad472c8cb7b8d5e277f4e6d8"
 
 abab@^1.0.3:
   version "1.0.4"
@@ -47,7 +100,7 @@ acorn@^4.0.3, acorn@^4.0.4:
   version "4.0.13"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
 
-acorn@^5.0.0, acorn@^5.1.1:
+acorn@^5.0.0, acorn@^5.1.1, acorn@^5.2.1:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7"
 
@@ -79,8 +132,8 @@ ajv@^4.7.0, ajv@^4.9.1:
     json-stable-stringify "^1.0.1"
 
 ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5:
-  version "5.3.0"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.3.0.tgz#4414ff74a50879c208ee5fdc826e32c303549eda"
+  version "5.5.1"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.1.tgz#b38bb8876d9e86bee994956a04e721e88b248eb2"
   dependencies:
     co "^4.6.0"
     fast-deep-equal "^1.0.0"
@@ -236,8 +289,8 @@ asap@~2.0.3:
   resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
 
 asn1.js@^4.0.0:
-  version "4.9.1"
-  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40"
+  version "4.9.2"
+  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a"
   dependencies:
     bn.js "^4.0.0"
     inherits "^2.0.1"
@@ -277,6 +330,10 @@ async-foreach@^0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
 
+async-limiter@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
+
 async@2.4.1:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
@@ -288,8 +345,8 @@ async@^1.4.0, async@^1.5.2:
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
 async@^2.1.2, async@^2.1.4, async@^2.1.5, async@^2.4.1:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
   dependencies:
     lodash "^4.14.0"
 
@@ -346,14 +403,6 @@ axios@~0.16.2:
     follow-redirects "^1.2.3"
     is-buffer "^1.1.5"
 
-babel-code-frame@7.0.0-beta.0:
-  version "7.0.0-beta.0"
-  resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-7.0.0-beta.0.tgz#418a7b5f3f7dc9a4670e61b1158b4c5661bec98d"
-  dependencies:
-    chalk "^2.0.0"
-    esutils "^2.0.2"
-    js-tokens "^3.0.0"
-
 babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
@@ -396,13 +445,13 @@ babel-eslint@^7.2.3:
     babylon "^6.17.0"
 
 babel-eslint@^8.0.1:
-  version "8.0.1"
-  resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.0.1.tgz#5d718be7a328625d006022eb293ed3008cbd6346"
+  version "8.0.3"
+  resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.0.3.tgz#f29ecf02336be438195325cd47c468da81ee4e98"
   dependencies:
-    babel-code-frame "7.0.0-beta.0"
-    babel-traverse "7.0.0-beta.0"
-    babel-types "7.0.0-beta.0"
-    babylon "7.0.0-beta.22"
+    "@babel/code-frame" "7.0.0-beta.31"
+    "@babel/traverse" "7.0.0-beta.31"
+    "@babel/types" "7.0.0-beta.31"
+    babylon "7.0.0-beta.31"
 
 babel-generator@^6.18.0, babel-generator@^6.26.0:
   version "6.26.0"
@@ -459,15 +508,6 @@ babel-helper-explode-assignable-expression@^6.24.1:
     babel-traverse "^6.24.1"
     babel-types "^6.24.1"
 
-babel-helper-function-name@7.0.0-beta.0:
-  version "7.0.0-beta.0"
-  resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-7.0.0-beta.0.tgz#d1b6779b647e5c5c31ebeb05e13b998e4d352d56"
-  dependencies:
-    babel-helper-get-function-arity "7.0.0-beta.0"
-    babel-template "7.0.0-beta.0"
-    babel-traverse "7.0.0-beta.0"
-    babel-types "7.0.0-beta.0"
-
 babel-helper-function-name@^6.24.1:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
@@ -478,12 +518,6 @@ babel-helper-function-name@^6.24.1:
     babel-traverse "^6.24.1"
     babel-types "^6.24.1"
 
-babel-helper-get-function-arity@7.0.0-beta.0:
-  version "7.0.0-beta.0"
-  resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-7.0.0-beta.0.tgz#9d1ab7213bb5efe1ef1638a8ea1489969b5a8b6e"
-  dependencies:
-    babel-types "7.0.0-beta.0"
-
 babel-helper-get-function-arity@^6.24.1:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
@@ -498,6 +532,13 @@ babel-helper-hoist-variables@^6.24.1:
     babel-runtime "^6.22.0"
     babel-types "^6.24.1"
 
+babel-helper-module-imports@^7.0.0-beta.3:
+  version "7.0.0-beta.3"
+  resolved "https://registry.yarnpkg.com/babel-helper-module-imports/-/babel-helper-module-imports-7.0.0-beta.3.tgz#e15764e3af9c8e11810c09f78f498a2bdc71585a"
+  dependencies:
+    babel-types "7.0.0-beta.3"
+    lodash "^4.2.0"
+
 babel-helper-optimise-call-expression@^6.24.1:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
@@ -562,10 +603,6 @@ babel-macros@^1.1.1:
   dependencies:
     cosmiconfig "3.1.0"
 
-babel-messages@7.0.0-beta.0:
-  version "7.0.0-beta.0"
-  resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-7.0.0-beta.0.tgz#6df01296e49fc8fbd0637394326a167f36da817b"
-
 babel-messages@^6.23.0:
   version "6.23.0"
   resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
@@ -590,18 +627,20 @@ babel-plugin-jest-hoist@^21.2.0:
   version "21.2.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-21.2.0.tgz#2cef637259bd4b628a6cace039de5fcd14dbb006"
 
-babel-plugin-lodash@^3.2.11:
-  version "3.2.11"
-  resolved "https://registry.yarnpkg.com/babel-plugin-lodash/-/babel-plugin-lodash-3.2.11.tgz#21c8fdec9fe1835efaa737873e3902bdd66d5701"
+babel-plugin-lodash@^3.3.2:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/babel-plugin-lodash/-/babel-plugin-lodash-3.3.2.tgz#da3a5b49ba27447f54463f6c4fa81396ccdd463f"
   dependencies:
+    babel-helper-module-imports "^7.0.0-beta.3"
+    babel-types "^6.26.0"
     glob "^7.1.1"
-    lodash "^4.17.2"
+    lodash "^4.17.4"
+    require-package-name "^2.0.1"
 
 babel-plugin-preval@^1.6.1:
-  version "1.6.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-preval/-/babel-plugin-preval-1.6.1.tgz#c33fd124f9cb9281550cae35ff0ea6065e32261d"
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/babel-plugin-preval/-/babel-plugin-preval-1.6.2.tgz#8f580a1d4579d5fc79f1cfaee6f9fe0996fdeb1f"
   dependencies:
-    babel-core "^6.26.0"
     babel-macros "^1.1.1"
     babel-register "^6.26.0"
     babylon "^6.18.0"
@@ -1001,15 +1040,6 @@ babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtim
     core-js "^2.4.0"
     regenerator-runtime "^0.11.0"
 
-babel-template@7.0.0-beta.0:
-  version "7.0.0-beta.0"
-  resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-7.0.0-beta.0.tgz#85083cf9e4395d5e48bf5154d7a8d6991cafecfb"
-  dependencies:
-    babel-traverse "7.0.0-beta.0"
-    babel-types "7.0.0-beta.0"
-    babylon "7.0.0-beta.22"
-    lodash "^4.2.0"
-
 babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
@@ -1020,20 +1050,6 @@ babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0, babel-te
     babylon "^6.18.0"
     lodash "^4.17.4"
 
-babel-traverse@7.0.0-beta.0:
-  version "7.0.0-beta.0"
-  resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-7.0.0-beta.0.tgz#da14be9b762f62a2f060db464eaafdd8cd072a41"
-  dependencies:
-    babel-code-frame "7.0.0-beta.0"
-    babel-helper-function-name "7.0.0-beta.0"
-    babel-messages "7.0.0-beta.0"
-    babel-types "7.0.0-beta.0"
-    babylon "7.0.0-beta.22"
-    debug "^3.0.1"
-    globals "^10.0.0"
-    invariant "^2.2.0"
-    lodash "^4.2.0"
-
 babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
@@ -1048,9 +1064,9 @@ babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-tr
     invariant "^2.2.2"
     lodash "^4.17.4"
 
-babel-types@7.0.0-beta.0:
-  version "7.0.0-beta.0"
-  resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-7.0.0-beta.0.tgz#eb8b6e556470e6dcc4aef982d79ad229469b5169"
+babel-types@7.0.0-beta.3:
+  version "7.0.0-beta.3"
+  resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-7.0.0-beta.3.tgz#cd927ca70e0ae8ab05f4aab83778cfb3e6eb20b4"
   dependencies:
     esutils "^2.0.2"
     lodash "^4.2.0"
@@ -1065,9 +1081,9 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24
     lodash "^4.17.4"
     to-fast-properties "^1.0.3"
 
-babylon@7.0.0-beta.22:
-  version "7.0.0-beta.22"
-  resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.22.tgz#74f0ad82ed7c7c3cfeab74cf684f815104161b65"
+babylon@7.0.0-beta.31:
+  version "7.0.0-beta.31"
+  resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.31.tgz#7ec10f81e0e456fd0f855ad60fa30c2ac454283f"
 
 babylon@^6.17.0, babylon@^6.18.0:
   version "6.18.0"
@@ -1110,8 +1126,8 @@ big.js@^3.1.3:
   resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
 
 binary-extensions@^1.0.0:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0"
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
 
 block-stream@*:
   version "0.0.9"
@@ -1242,11 +1258,11 @@ browserify-sign@^4.0.0:
     inherits "^2.0.1"
     parse-asn1 "^5.0.0"
 
-browserify-zlib@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d"
+browserify-zlib@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
   dependencies:
-    pako "~0.2.0"
+    pako "~1.0.5"
 
 browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
   version "1.7.7"
@@ -1256,10 +1272,10 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
     electron-to-chromium "^1.2.7"
 
 browserslist@^2.1.2, browserslist@^2.5.1:
-  version "2.6.1"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.6.1.tgz#cc65a05ad6131ebda26f076f2822ba1bc826376b"
+  version "2.9.1"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.9.1.tgz#b72d3982ab01b5cd24da62ff6d45573886aff275"
   dependencies:
-    caniuse-lite "^1.0.30000755"
+    caniuse-lite "^1.0.30000770"
     electron-to-chromium "^1.3.27"
 
 bser@^2.0.0:
@@ -1351,12 +1367,16 @@ caniuse-api@^1.5.2:
     lodash.uniq "^4.5.0"
 
 caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
-  version "1.0.30000755"
-  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000755.tgz#a08c547c39dbe4ad07dcca9763fcbbff0c891de0"
+  version "1.0.30000777"
+  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000777.tgz#2e19adba63bdd7c501df637a862adead7f4bc054"
+
+caniuse-lite@^1.0.30000748, caniuse-lite@^1.0.30000770:
+  version "1.0.30000777"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000777.tgz#31c18a4a8cd49782ebb305c8e8a93e6b3b3e4f13"
 
-caniuse-lite@^1.0.30000748, caniuse-lite@^1.0.30000755:
-  version "1.0.30000755"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000755.tgz#9ce5f6e06bd75ec8209abe8853c3beef02248d65"
+caseless@~0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
 
 caseless@~0.12.0:
   version "0.12.0"
@@ -1383,7 +1403,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
     strip-ansi "^3.0.0"
     supports-color "^2.0.0"
 
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
   dependencies:
@@ -1418,8 +1438,8 @@ chokidar@^1.6.0, chokidar@^1.7.0:
     fsevents "^1.0.0"
 
 ci-info@^1.0.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.1.tgz#47b44df118c48d2597b56d342e7e25791060171a"
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.2.tgz#03561259db48d0474c8bdc90f5b47b068b6bbfb4"
 
 cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
   version "1.0.4"
@@ -1478,8 +1498,8 @@ clone-deep@^0.3.0:
     shallow-clone "^0.1.2"
 
 clone@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f"
 
 co@^4.6.0:
   version "4.6.0"
@@ -1496,8 +1516,8 @@ code-point-at@^1.0.0:
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
 
 color-convert@^1.3.0, color-convert@^1.9.0:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
   dependencies:
     color-name "^1.1.1"
 
@@ -1542,8 +1562,8 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
     delayed-stream "~1.0.0"
 
 commander@^2.8.1, commander@^2.9.0:
-  version "2.11.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
+  version "2.12.2"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
 
 commondir@^1.0.1:
   version "1.0.1"
@@ -1591,8 +1611,8 @@ concat-stream@^1.5.2:
     typedarray "^0.0.6"
 
 connect-history-api-fallback@^1.3.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.4.0.tgz#3db24f973f4b923b0e82f619ce0df02411ca623d"
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a"
 
 console-browserify@^1.1.0:
   version "1.1.0"
@@ -1629,8 +1649,8 @@ convert-source-map@^0.3.3:
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190"
 
 convert-source-map@^1.1.1, convert-source-map@^1.4.0, convert-source-map@^1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
 
 cookie-signature@1.0.6:
   version "1.0.6"
@@ -1743,8 +1763,8 @@ cryptiles@3.x.x:
     boom "5.x.x"
 
 crypto-browserify@^3.11.0:
-  version "3.11.1"
-  resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f"
+  version "3.12.0"
+  resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
   dependencies:
     browserify-cipher "^1.0.0"
     browserify-sign "^4.0.0"
@@ -1756,8 +1776,9 @@ crypto-browserify@^3.11.0:
     pbkdf2 "^3.0.3"
     public-encrypt "^4.0.0"
     randombytes "^2.0.0"
+    randomfill "^1.0.3"
 
-css-color-function@^1.3.0:
+css-color-function@~1.3.3:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.3.tgz#8ed24c2c0205073339fafa004bc8c141fccb282e"
   dependencies:
@@ -1933,7 +1954,7 @@ date-now@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
 
-debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
+debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.6.6, debug@^2.6.8:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   dependencies:
@@ -2040,6 +2061,10 @@ detect-indent@^4.0.0:
   dependencies:
     repeating "^2.0.0"
 
+detect-libc@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+
 detect-node@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127"
@@ -2089,11 +2114,10 @@ doctrine@1.5.0, doctrine@^1.2.2:
     isarray "^1.0.0"
 
 doctrine@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.2.tgz#68f96ce8efc56cc42651f1faadb4f175273b0075"
   dependencies:
     esutils "^2.0.2"
-    isarray "^1.0.0"
 
 dom-helpers@^3.2.0, dom-helpers@^3.2.1:
   version "3.2.1"
@@ -2215,39 +2239,40 @@ entities@^1.1.1, entities@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
 
-enzyme-adapter-react-16@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.2.tgz#8c6f431f17c69e1e9eeb25ca4bd92f31971eb2dd"
+enzyme-adapter-react-16@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.0.tgz#86c5db7c10f0be6ec25d54ca41b59f2abb397cf4"
   dependencies:
-    enzyme-adapter-utils "^1.0.0"
+    enzyme-adapter-utils "^1.1.0"
     lodash "^4.17.4"
     object.assign "^4.0.4"
     object.values "^1.0.4"
     prop-types "^15.5.10"
     react-test-renderer "^16.0.0-0"
 
-enzyme-adapter-utils@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.0.1.tgz#fcd81223339a55a312f7552641e045c404084009"
+enzyme-adapter-utils@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.2.0.tgz#7f4471ee0a70b91169ec8860d2bf0a6b551664b2"
   dependencies:
     lodash "^4.17.4"
     object.assign "^4.0.4"
     prop-types "^15.5.10"
 
-enzyme@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.1.0.tgz#d8ca84085790fbcec6ed40badd14478faee4c25a"
+enzyme@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.2.0.tgz#998bdcda0fc71b8764a0017f7cc692c943f54a7a"
   dependencies:
     cheerio "^1.0.0-rc.2"
     function.prototype.name "^1.0.3"
+    has "^1.0.1"
     is-subset "^0.1.1"
     lodash "^4.17.4"
     object-is "^1.0.1"
     object.assign "^4.0.4"
     object.entries "^1.0.4"
     object.values "^1.0.4"
-    raf "^3.3.2"
-    rst-selector-parser "^2.2.2"
+    raf "^3.4.0"
+    rst-selector-parser "^2.2.3"
 
 errno@^0.1.3, errno@^0.1.4:
   version "0.1.4"
@@ -2262,8 +2287,8 @@ error-ex@^1.2.0, error-ex@^1.3.1:
     is-arrayish "^0.2.1"
 
 es-abstract@^1.6.1, es-abstract@^1.7.0:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.9.0.tgz#690829a07cae36b222e7fd9b75c0d0573eb25227"
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864"
   dependencies:
     es-to-primitive "^1.1.1"
     function-bind "^1.1.1"
@@ -2280,8 +2305,8 @@ es-to-primitive@^1.1.1:
     is-symbol "^1.0.1"
 
 es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
-  version "0.10.35"
-  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.35.tgz#18ee858ce6a3c45c7d79e91c15fcca9ec568494f"
+  version "0.10.37"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.37.tgz#0ee741d148b80069ba27d020393756af257defc3"
   dependencies:
     es6-iterator "~2.0.1"
     es6-symbol "~3.1.1"
@@ -2450,10 +2475,10 @@ eslint@^3.19.0:
     user-home "^2.0.0"
 
 espree@^3.4.0:
-  version "3.5.1"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.1.tgz#0c988b8ab46db53100a1954ae4ba995ddd27d87e"
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca"
   dependencies:
-    acorn "^5.1.1"
+    acorn "^5.2.1"
     acorn-jsx "^3.0.0"
 
 esprima@^2.6.0:
@@ -2566,7 +2591,7 @@ expect@^21.2.1:
     jest-message-util "^21.2.1"
     jest-regex-util "^21.2.0"
 
-express@^4.13.3, express@^4.15.2, express@^4.16.2:
+express@^4.15.2, express@^4.16.2:
   version "4.16.2"
   resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c"
   dependencies:
@@ -2620,10 +2645,14 @@ extract-text-webpack-plugin@^3.0.2:
     schema-utils "^0.3.0"
     webpack-sources "^1.0.1"
 
-extsprintf@1.3.0, extsprintf@^1.2.0:
+extsprintf@1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
 
+extsprintf@^1.2.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+
 fast-deep-equal@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
@@ -2762,10 +2791,10 @@ flatten@^1.0.2:
   resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
 
 follow-redirects@^1.2.3:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.5.tgz#ffd3e14cbdd5eaa72f61b6368c1f68516c2a26cc"
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.6.tgz#4dcdc7e4ab3dd6765a97ff89c3b4c258117c79bf"
   dependencies:
-    debug "^2.6.9"
+    debug "^3.1.0"
 
 font-awesome@^4.7.0:
   version "4.7.0"
@@ -2842,11 +2871,11 @@ fs.realpath@^1.0.0:
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
 
 fsevents@*, fsevents@^1.0.0, fsevents@^1.1.1:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4"
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8"
   dependencies:
     nan "^2.3.0"
-    node-pre-gyp "^0.6.36"
+    node-pre-gyp "^0.6.39"
 
 fstream-ignore@^1.0.5:
   version "1.0.5"
@@ -2941,6 +2970,16 @@ glob-parent@^2.0.0:
   dependencies:
     is-glob "^2.0.0"
 
+glob@^6.0.4:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
+  dependencies:
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "2 || 3"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
 glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
   version "7.1.2"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
@@ -2953,8 +2992,8 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
     path-is-absolute "^1.0.0"
 
 globals@^10.0.0:
-  version "10.2.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-10.2.0.tgz#69490789091fcaa7f7d512c668c8eb73894a4ef2"
+  version "10.4.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-10.4.0.tgz#5c477388b128a9e4c5c5d01c7a2aca68c68b2da7"
 
 globals@^9.14.0, globals@^9.18.0:
   version "9.18.0"
@@ -2990,8 +3029,8 @@ globule@^1.0.0:
     minimatch "~3.0.2"
 
 gonzales-pe@^4.0.3:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.2.2.tgz#f50a8c17842f13a9007909b7cb32188266e4d74c"
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.2.3.tgz#41091703625433285e0aee3aa47829fc1fbeb6f2"
   dependencies:
     minimist "1.1.x"
 
@@ -3031,6 +3070,15 @@ har-schema@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
 
+har-validator@~2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
+  dependencies:
+    chalk "^1.1.1"
+    commander "^2.9.0"
+    is-my-json-valid "^2.12.4"
+    pinkie-promise "^2.0.0"
+
 har-validator@~4.2.1:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
@@ -3235,9 +3283,9 @@ http-signature@~1.2.0:
     jsprim "^1.2.2"
     sshpk "^1.7.0"
 
-https-browserify@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
+https-browserify@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
 
 iconv-lite@0.4.19, iconv-lite@~0.4.13:
   version "0.4.19"
@@ -3310,8 +3358,8 @@ inherits@2.0.1:
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
 
 ini@~1.3.0:
-  version "1.3.4"
-  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
 
 inquirer@^0.12.0:
   version "0.12.0"
@@ -3338,30 +3386,26 @@ internal-ip@1.2.0:
     meow "^3.3.0"
 
 interpret@^1.0.0:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
 
-intersection-observer@^0.4.0:
-  version "0.4.2"
-  resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.4.2.tgz#24100ed620baf6a427072996d4d73366e9ec93ef"
+intersection-observer@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.5.0.tgz#9fe8bee3953c755b1485c38efd9633d535775ea6"
 
 intl-format-cache@^2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-2.0.5.tgz#b484cefcb9353f374f25de389a3ceea1af18d7c9"
-
-intl-messageformat-parser@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.2.0.tgz#5906b7f953ab7470e0dc8549097b648b991892ff"
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-2.1.0.tgz#04a369fecbfad6da6005bae1f14333332dcf9316"
 
-intl-messageformat-parser@^1.2.0:
+intl-messageformat-parser@1.4.0, intl-messageformat-parser@^1.2.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz#b43d45a97468cadbe44331d74bb1e8dea44fc075"
 
-intl-messageformat@^2.0.0, intl-messageformat@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.1.0.tgz#1c51da76f02a3f7b360654cdc51bbc4d3fa6c72c"
+intl-messageformat@^2.0.0, intl-messageformat@^2.1.0, intl-messageformat@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.2.0.tgz#345bcd46de630b7683330c2e52177ff5eab484fc"
   dependencies:
-    intl-messageformat-parser "1.2.0"
+    intl-messageformat-parser "1.4.0"
 
 intl-relativeformat@^2.0.0, intl-relativeformat@^2.1.0:
   version "2.1.0"
@@ -3483,7 +3527,7 @@ is-glob@^3.1.0:
   dependencies:
     is-extglob "^2.1.0"
 
-is-my-json-valid@^2.10.0:
+is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4:
   version "2.16.1"
   resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11"
   dependencies:
@@ -3521,8 +3565,8 @@ is-path-in-cwd@^1.0.0:
     is-path-inside "^1.0.0"
 
 is-path-inside@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
   dependencies:
     path-is-inside "^1.0.1"
 
@@ -3920,8 +3964,8 @@ jest@^21.2.1:
     jest-cli "^21.2.1"
 
 js-base64@^2.1.8, js-base64@^2.1.9:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf"
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.0.tgz#9e566fee624751a1d720c966cd6226d29d4025aa"
 
 js-string-escape@1.0.1:
   version "1.0.1"
@@ -4042,6 +4086,10 @@ keycode@^2.1.7:
   version "2.1.9"
   resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa"
 
+killable@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b"
+
 kind-of@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5"
@@ -4253,8 +4301,8 @@ lodash.uniq@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
 
 loglevel@^1.4.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.5.1.tgz#189078c94ab9053ee215a0acdbf24244ea0f6502"
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.0.tgz#ae0caa561111498c5ba13723d6fb631d24003934"
 
 longest@^1.0.1:
   version "1.0.1"
@@ -4313,8 +4361,8 @@ math-expression-evaluator@^1.2.14:
   resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
 
 mathjs@^3.11.5:
-  version "3.16.5"
-  resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-3.16.5.tgz#d75a5265435d2824b067b37a478771deebf6aacc"
+  version "3.17.0"
+  resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-3.17.0.tgz#9569d278874546175c9d0497d7417eb88fb61503"
   dependencies:
     complex.js "2.0.4"
     decimal.js "7.2.3"
@@ -4322,7 +4370,7 @@ mathjs@^3.11.5:
     javascript-natural-sort "0.7.1"
     seed-random "2.2.0"
     tiny-emitter "2.0.0"
-    typed-function "0.10.5"
+    typed-function "0.10.6"
 
 md5.js@^1.3.4:
   version "1.3.4"
@@ -4401,8 +4449,8 @@ miller-rabin@^4.0.0:
     brorand "^1.0.1"
 
 "mime-db@>= 1.30.0 < 2":
-  version "1.31.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.31.0.tgz#a49cd8f3ebf3ed1a482b60561d9105ad40ca74cb"
+  version "1.32.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.32.0.tgz#485b3848b01a3cda5f968b4882c0771e58e09414"
 
 mime-db@~1.30.0:
   version "1.30.0"
@@ -4414,10 +4462,14 @@ mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17,
   dependencies:
     mime-db "~1.30.0"
 
-mime@1.4.1, mime@^1.3.4:
+mime@1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
 
+mime@^1.5.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+
 mimic-fn@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
@@ -4430,7 +4482,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
 
-minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   dependencies:
@@ -4478,8 +4530,8 @@ multicast-dns-service-types@^1.1.0:
   resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
 
 multicast-dns@^6.0.1:
-  version "6.1.1"
-  resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.1.1.tgz#6e7de86a570872ab17058adea7160bbeca814dde"
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.1.tgz#c5035defa9219d30640558a49298067352098060"
   dependencies:
     dns-packet "^1.0.1"
     thunky "^0.1.0"
@@ -4489,8 +4541,8 @@ mute-stream@0.0.5:
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
 
 nan@^2.0.0, nan@^2.3.0, nan@^2.3.2:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
 
 natural-compare@^1.4.0:
   version "1.4.0"
@@ -4542,28 +4594,28 @@ node-int64@^0.4.0:
   resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
 
 node-libs-browser@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646"
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
   dependencies:
     assert "^1.1.1"
-    browserify-zlib "^0.1.4"
+    browserify-zlib "^0.2.0"
     buffer "^4.3.0"
     console-browserify "^1.1.0"
     constants-browserify "^1.0.0"
     crypto-browserify "^3.11.0"
     domain-browser "^1.1.1"
     events "^1.0.0"
-    https-browserify "0.0.1"
-    os-browserify "^0.2.0"
+    https-browserify "^1.0.0"
+    os-browserify "^0.3.0"
     path-browserify "0.0.0"
-    process "^0.11.0"
+    process "^0.11.10"
     punycode "^1.2.4"
     querystring-es3 "^0.2.0"
-    readable-stream "^2.0.5"
+    readable-stream "^2.3.3"
     stream-browserify "^2.0.1"
-    stream-http "^2.3.1"
-    string_decoder "^0.10.25"
-    timers-browserify "^2.0.2"
+    stream-http "^2.7.2"
+    string_decoder "^1.0.0"
+    timers-browserify "^2.0.4"
     tty-browserify "0.0.0"
     url "^0.11.0"
     util "^0.10.3"
@@ -4578,10 +4630,11 @@ node-notifier@^5.0.2:
     shellwords "^0.1.0"
     which "^1.2.12"
 
-node-pre-gyp@^0.6.36, node-pre-gyp@^0.6.4:
-  version "0.6.38"
-  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.38.tgz#e92a20f83416415bb4086f6d1fb78b3da73d113d"
+node-pre-gyp@^0.6.39, node-pre-gyp@^0.6.4:
+  version "0.6.39"
+  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649"
   dependencies:
+    detect-libc "^1.0.2"
     hawk "3.1.3"
     mkdirp "^0.5.1"
     nopt "^4.0.1"
@@ -4593,9 +4646,9 @@ node-pre-gyp@^0.6.36, node-pre-gyp@^0.6.4:
     tar "^2.2.1"
     tar-pack "^3.4.0"
 
-node-sass@^4.5.2:
-  version "4.5.3"
-  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.3.tgz#d09c9d1179641239d1b97ffc6231fdcec53e1568"
+node-sass@^4.7.2:
+  version "4.7.2"
+  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e"
   dependencies:
     async-foreach "^0.1.3"
     chalk "^1.1.1"
@@ -4612,9 +4665,10 @@ node-sass@^4.5.2:
     nan "^2.3.2"
     node-gyp "^3.3.1"
     npmlog "^4.0.0"
-    request "^2.79.0"
-    sass-graph "^2.1.1"
+    request "~2.79.0"
+    sass-graph "^2.2.4"
     stdout-stream "^1.4.0"
+    "true-case-path" "^1.0.2"
 
 node-zopfli@^2.0.2:
   version "2.0.2"
@@ -4835,9 +4889,9 @@ original@>=0.0.5:
   dependencies:
     url-parse "1.0.x"
 
-os-browserify@^0.2.0:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
+os-browserify@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
 
 os-homedir@^1.0.0, os-homedir@^1.0.1:
   version "1.0.2"
@@ -4894,9 +4948,9 @@ packet-reader@0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-0.3.1.tgz#cd62e60af8d7fea8a705ec4ff990871c46871f27"
 
-pako@~0.2.0:
-  version "0.2.9"
-  resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
+pako@~1.0.5:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
 
 parse-asn1@^5.0.0:
   version "5.1.0"
@@ -4948,10 +5002,10 @@ parse5@^1.5.1:
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
 
 parse5@^3.0.1:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.2.tgz#05eff57f0ef4577fb144a79f8b9a967a6cc44510"
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
   dependencies:
-    "@types/node" "^6.0.46"
+    "@types/node" "*"
 
 parseurl@~1.3.2:
   version "1.3.2"
@@ -5037,6 +5091,10 @@ pg-connection-string@0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7"
 
+pg-int8@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
+
 pg-pool@1.*:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-1.8.0.tgz#f7ec73824c37a03f076f51bfdf70e340147c4f37"
@@ -5045,9 +5103,10 @@ pg-pool@1.*:
     object-assign "4.1.0"
 
 pg-types@1.*:
-  version "1.12.1"
-  resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.12.1.tgz#d64087e3903b58ffaad279e7595c52208a14c3d2"
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.13.0.tgz#75f490b8a8abf75f1386ef5ec4455ecf6b345c63"
   dependencies:
+    pg-int8 "1.0.1"
     postgres-array "~1.0.0"
     postgres-bytea "~1.0.0"
     postgres-date "~1.0.0"
@@ -5135,10 +5194,10 @@ postcss-calc@^5.2.0:
     reduce-css-calc "^1.2.6"
 
 postcss-color-function@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.0.0.tgz#7e0106f4f6a1ecb1ad5b3a8553ace5e828aae187"
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.0.1.tgz#402b3f2cebc3f6947e618fb6be3654fbecef6444"
   dependencies:
-    css-color-function "^1.3.0"
+    css-color-function "~1.3.3"
     postcss "^6.0.1"
     postcss-message-helpers "^2.0.0"
     postcss-value-parser "^3.3.0"
@@ -5262,9 +5321,9 @@ postcss-load-plugins@^2.3.0:
     cosmiconfig "^2.1.1"
     object-assign "^4.1.0"
 
-postcss-loader@^2.0.8:
-  version "2.0.8"
-  resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.8.tgz#8c67ddb029407dfafe684a406cfc16bad2ce0814"
+postcss-loader@^2.0.9:
+  version "2.0.9"
+  resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.9.tgz#001fdf7bfeeb159405ee61d1bb8e59b528dbd309"
   dependencies:
     loader-utils "^1.1.0"
     postcss "^6.0.0"
@@ -5349,8 +5408,8 @@ postcss-mixins@^6.0.1:
     sugarss "^1.0.0"
 
 postcss-modules-extract-imports@^1.0.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz#b614c9720be6816eaee35fb3a5faa1dba6a05ddb"
   dependencies:
     postcss "^6.0.1"
 
@@ -5550,11 +5609,11 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
     source-map "^0.5.6"
     supports-color "^3.2.3"
 
-postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.13, postcss@^6.0.3, postcss@^6.0.6, postcss@^6.0.9:
-  version "6.0.13"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.13.tgz#b9ecab4ee00c89db3ec931145bd9590bbf3f125f"
+postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.13, postcss@^6.0.14, postcss@^6.0.3, postcss@^6.0.6, postcss@^6.0.9:
+  version "6.0.14"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.14.tgz#5534c72114739e75d0afcf017db853099f562885"
   dependencies:
-    chalk "^2.1.0"
+    chalk "^2.3.0"
     source-map "^0.6.1"
     supports-color "^4.4.0"
 
@@ -5628,7 +5687,7 @@ process-nextick-args@~1.0.6:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
 
-process@^0.11.0:
+process@^0.11.10:
   version "0.11.10"
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
 
@@ -5707,6 +5766,10 @@ qs@6.5.1, qs@~6.5.1:
   version "6.5.1"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
 
+qs@~6.3.0:
+  version "6.3.2"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
+
 qs@~6.4.0:
   version "6.4.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
@@ -5738,7 +5801,7 @@ quote@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/quote/-/quote-0.4.0.tgz#10839217f6c1362b89194044d29b233fd7f32f01"
 
-raf@^3.1.0, raf@^3.3.2, raf@^3.4.0:
+raf@^3.1.0, raf@^3.4.0:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
   dependencies:
@@ -5766,12 +5829,19 @@ randomatic@^1.1.3:
     is-number "^3.0.0"
     kind-of "^4.0.0"
 
-randombytes@^2.0.0, randombytes@^2.0.1:
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79"
   dependencies:
     safe-buffer "^5.1.0"
 
+randomfill@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62"
+  dependencies:
+    randombytes "^2.0.5"
+    safe-buffer "^5.1.0"
+
 range-parser@^1.0.3, range-parser@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
@@ -5794,16 +5864,16 @@ rc@^1.1.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
-react-dom@^16.0.0:
-  version "16.0.0"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.0.0.tgz#9cc3079c3dcd70d4c6e01b84aab2a7e34c303f58"
+react-dom@^16.2.0:
+  version "16.2.0"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044"
   dependencies:
     fbjs "^0.8.16"
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
     prop-types "^15.6.0"
 
-react-event-listener@^0.5.0:
+react-event-listener@^0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.5.1.tgz#ba36076e47bc37c5a67ff5ccd4a9ff0f15621040"
   dependencies:
@@ -5920,47 +5990,48 @@ react-router@^4.2.0:
     prop-types "^15.5.4"
     warning "^3.0.0"
 
-react-swipeable-views-core@^0.12.8:
-  version "0.12.8"
-  resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.12.8.tgz#99460621e5a6da07fb482a25b151905ae7a797a9"
+react-swipeable-views-core@^0.12.11:
+  version "0.12.11"
+  resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.12.11.tgz#3cf2b4daffbb36f9d69bd19bf5b2d5370b6b2c1b"
   dependencies:
     babel-runtime "^6.23.0"
     warning "^3.0.0"
 
-react-swipeable-views-utils@^0.12.8:
-  version "0.12.8"
-  resolved "https://registry.yarnpkg.com/react-swipeable-views-utils/-/react-swipeable-views-utils-0.12.8.tgz#9483fc7dd370032f2f93ac44f2a2913d7c52aa41"
+react-swipeable-views-utils@^0.12.11:
+  version "0.12.11"
+  resolved "https://registry.yarnpkg.com/react-swipeable-views-utils/-/react-swipeable-views-utils-0.12.11.tgz#3c9a6a2b8dbdcc331a5d107419578f57b7e101d6"
   dependencies:
     babel-runtime "^6.23.0"
     fbjs "^0.8.4"
     keycode "^2.1.7"
-    prop-types "^15.5.4"
-    react-event-listener "^0.5.0"
-    react-swipeable-views-core "^0.12.8"
+    prop-types "^15.6.0"
+    react-event-listener "^0.5.1"
+    react-swipeable-views-core "^0.12.11"
 
 react-swipeable-views@^0.12.3:
-  version "0.12.8"
-  resolved "https://registry.yarnpkg.com/react-swipeable-views/-/react-swipeable-views-0.12.8.tgz#8541daab5881067e58281d1e6ff13815ae94ebf5"
+  version "0.12.12"
+  resolved "https://registry.yarnpkg.com/react-swipeable-views/-/react-swipeable-views-0.12.12.tgz#60cdc8e3682ed082aaf094f7761eaf691ed28a6f"
   dependencies:
     babel-runtime "^6.23.0"
     dom-helpers "^3.2.1"
     prop-types "^15.5.4"
-    react-swipeable-views-core "^0.12.8"
-    react-swipeable-views-utils "^0.12.8"
+    react-swipeable-views-core "^0.12.11"
+    react-swipeable-views-utils "^0.12.11"
     warning "^3.0.0"
 
-react-test-renderer@^16.0.0, react-test-renderer@^16.0.0-0:
-  version "16.0.0"
-  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.0.0.tgz#9fe7b8308f2f71f29fc356d4102086f131c9cb15"
+react-test-renderer@^16.0.0-0, react-test-renderer@^16.2.0:
+  version "16.2.0"
+  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.2.0.tgz#bddf259a6b8fcd8555f012afc8eacc238872a211"
   dependencies:
     fbjs "^0.8.16"
     object-assign "^4.1.1"
+    prop-types "^15.6.0"
 
-react-textarea-autosize@^5.0.7:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-5.1.0.tgz#ffbf8164fce217c79443c1c17dedf730592df224"
+react-textarea-autosize@^5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-5.2.1.tgz#2b78f9067180f41b08ac59f78f1581abadd61e54"
   dependencies:
-    prop-types "^15.5.10"
+    prop-types "^15.6.0"
 
 react-toggle@^4.0.1:
   version "4.0.2"
@@ -5979,9 +6050,9 @@ react-transition-group@^2.2.0:
     prop-types "^15.5.8"
     warning "^3.0.0"
 
-react@^16.0.0:
-  version "16.0.0"
-  resolved "https://registry.yarnpkg.com/react/-/react-16.0.0.tgz#ce7df8f1941b036f02b2cca9dbd0cb1f0e855e2d"
+react@^16.2.0:
+  version "16.2.0"
+  resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
   dependencies:
     fbjs "^0.8.16"
     loose-envify "^1.1.0"
@@ -6024,7 +6095,7 @@ read-pkg@^2.0.0:
     normalize-package-data "^2.3.2"
     path-type "^2.0.0"
 
-readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.2.9:
+readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.2.9, readable-stream@^2.3.3:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
   dependencies:
@@ -6237,6 +6308,31 @@ request@2.81.0:
     tunnel-agent "^0.6.0"
     uuid "^3.0.0"
 
+request@~2.79.0:
+  version "2.79.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
+  dependencies:
+    aws-sign2 "~0.6.0"
+    aws4 "^1.2.1"
+    caseless "~0.11.0"
+    combined-stream "~1.0.5"
+    extend "~3.0.0"
+    forever-agent "~0.6.1"
+    form-data "~2.1.1"
+    har-validator "~2.0.6"
+    hawk "~3.1.3"
+    http-signature "~1.1.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.7"
+    oauth-sign "~0.8.1"
+    qs "~6.3.0"
+    stringstream "~0.0.4"
+    tough-cookie "~2.3.0"
+    tunnel-agent "~0.4.1"
+    uuid "^3.0.0"
+
 requestidlecallback@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/requestidlecallback/-/requestidlecallback-0.3.0.tgz#6fb74e0733f90df3faa4838f9f6a2a5f9b742ac5"
@@ -6257,6 +6353,10 @@ require-main-filename@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
 
+require-package-name@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9"
+
 require-uncached@^1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
@@ -6264,7 +6364,7 @@ require-uncached@^1.0.2:
     caller-path "^0.1.0"
     resolve-from "^1.0.0"
 
-requires-port@1.0.x, requires-port@1.x.x:
+requires-port@1.0.x, requires-port@1.x.x, requires-port@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
 
@@ -6363,7 +6463,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
     hash-base "^2.0.0"
     inherits "^2.0.1"
 
-rst-selector-parser@^2.2.2:
+rst-selector-parser@^2.2.3:
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
   dependencies:
@@ -6384,10 +6484,6 @@ safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
 
-safe-buffer@~5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
-
 sane@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/sane/-/sane-2.2.0.tgz#d6d2e2fcab00e3d283c93b912b7c3a20846f1d56"
@@ -6402,7 +6498,7 @@ sane@^2.0.0:
   optionalDependencies:
     fsevents "^1.1.1"
 
-sass-graph@^2.1.1:
+sass-graph@^2.2.4:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
   dependencies:
@@ -6432,8 +6528,8 @@ schema-utils@^0.3.0:
     ajv "^5.0.0"
 
 scroll-behavior@^0.9.1:
-  version "0.9.4"
-  resolved "https://registry.yarnpkg.com/scroll-behavior/-/scroll-behavior-0.9.4.tgz#73b4a0eae3e59c0b8f3b6fc1ff78f054a513e79c"
+  version "0.9.5"
+  resolved "https://registry.yarnpkg.com/scroll-behavior/-/scroll-behavior-0.9.5.tgz#41da30b559da004eb48450f6cff6068c7696ff23"
   dependencies:
     dom-helpers "^3.2.1"
     invariant "^2.2.2"
@@ -6651,11 +6747,11 @@ source-map@^0.4.2, source-map@^0.4.4:
   dependencies:
     amdefine ">=0.0.4"
 
-source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3, source-map@~0.5.6:
+source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.6:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
 
-source-map@^0.6.1:
+source-map@^0.6.1, source-map@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
 
@@ -6741,7 +6837,7 @@ stream-browserify@^2.0.1:
     inherits "~2.0.1"
     readable-stream "^2.0.2"
 
-stream-http@^2.3.1:
+stream-http@^2.7.2:
   version "2.7.2"
   resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad"
   dependencies:
@@ -6777,11 +6873,7 @@ string-width@^2.0.0:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^4.0.0"
 
-string_decoder@^0.10.25:
-  version "0.10.31"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
-
-string_decoder@~1.0.3:
+string_decoder@^1.0.0, string_decoder@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
   dependencies:
@@ -6791,9 +6883,9 @@ stringstream@~0.0.4, stringstream@~0.0.5:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
 
-stringz@^0.2.2:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/stringz/-/stringz-0.2.3.tgz#87bad6f5462c34bd73f84522c703f019d78f0b2d"
+stringz@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/stringz/-/stringz-0.3.0.tgz#58a311a8c791eee1a68f5f188b3db5e66ff49360"
 
 strip-ansi@^3.0.0, strip-ansi@^3.0.1:
   version "3.0.1"
@@ -6843,10 +6935,10 @@ substring-trie@^1.0.2:
   resolved "https://registry.yarnpkg.com/substring-trie/-/substring-trie-1.0.2.tgz#7b42592391628b4f2cb17365c6cce4257c7b7af5"
 
 sugarss@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-1.0.0.tgz#65e51b3958432fb70d5451a68bb33e32d0cf1ef7"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-1.0.1.tgz#be826d9003e0f247735f92365dc3fd7f1bae9e44"
   dependencies:
-    postcss "^6.0.0"
+    postcss "^6.0.14"
 
 supports-color@^2.0.0:
   version "2.0.0"
@@ -6877,8 +6969,8 @@ svgo@^0.7.0:
     whet.extend "~0.9.9"
 
 symbol-observable@^1.0.3:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.1.0.tgz#5c68fd8d54115d9dfb72a84720549222e8db9b32"
 
 symbol-tree@^3.2.1:
   version "3.2.2"
@@ -6960,7 +7052,7 @@ time-stamp@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357"
 
-timers-browserify@^2.0.2:
+timers-browserify@^2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6"
   dependencies:
@@ -7008,6 +7100,12 @@ trim-right@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
 
+"true-case-path@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62"
+  dependencies:
+    glob "^6.0.4"
+
 tryit@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
@@ -7022,6 +7120,10 @@ tunnel-agent@^0.6.0:
   dependencies:
     safe-buffer "^5.0.1"
 
+tunnel-agent@~0.4.1:
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
+
 tweetnacl@^0.14.3, tweetnacl@~0.14.0:
   version "0.14.5"
   resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
@@ -7039,9 +7141,9 @@ type-is@~1.6.15:
     media-typer "0.3.0"
     mime-types "~2.1.15"
 
-typed-function@0.10.5:
-  version "0.10.5"
-  resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-0.10.5.tgz#2e0f18abd065219fab694a446a65c6d1981832c0"
+typed-function@0.10.6:
+  version "0.10.6"
+  resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-0.10.6.tgz#314aa0ea72bd586de5920095559683e20b01688b"
 
 typedarray@^0.0.6:
   version "0.0.6"
@@ -7077,8 +7179,8 @@ uid-number@^0.0.6:
   resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
 
 ultron@~1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864"
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
 
 underscore@~1.4.4:
   version "1.4.4"
@@ -7118,11 +7220,11 @@ url-parse@1.0.x:
     requires-port "1.0.x"
 
 url-parse@^1.1.8:
-  version "1.1.9"
-  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.9.tgz#c67f1d775d51f0a18911dd7b3ffad27bb9e5bd19"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.2.0.tgz#3a19e8aaa6d023ddd27dcc44cb4fc8f7fec23986"
   dependencies:
     querystringify "~1.0.0"
-    requires-port "1.0.x"
+    requires-port "~1.0.0"
 
 url@^0.11.0:
   version "0.11.0"
@@ -7237,9 +7339,9 @@ webidl-conversions@^4.0.0:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
 
-webpack-bundle-analyzer@^2.8.3:
-  version "2.9.0"
-  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.9.0.tgz#b58bc34cc30b27ffdbaf3d00bf27aba6fa29c6e3"
+webpack-bundle-analyzer@^2.9.1:
+  version "2.9.1"
+  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.9.1.tgz#c2c8e03e8e5768ed288b39ae9e27a8b8d7b9d476"
   dependencies:
     acorn "^5.1.1"
     chalk "^1.1.3"
@@ -7251,21 +7353,21 @@ webpack-bundle-analyzer@^2.8.3:
     lodash "^4.17.4"
     mkdirp "^0.5.1"
     opener "^1.4.3"
-    ws "^2.3.1"
+    ws "^3.3.1"
 
 webpack-dev-middleware@^1.11.0:
-  version "1.12.0"
-  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz#d34efefb2edda7e1d3b5dbe07289513219651709"
+  version "1.12.2"
+  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz#f8fc1120ce3b4fc5680ceecb43d777966b21105e"
   dependencies:
     memory-fs "~0.4.1"
-    mime "^1.3.4"
+    mime "^1.5.0"
     path-is-absolute "^1.0.0"
     range-parser "^1.0.3"
     time-stamp "^2.0.0"
 
-webpack-dev-server@^2.9.3:
-  version "2.9.3"
-  resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.9.3.tgz#f0554e88d129e87796a6f74a016b991743ca6f81"
+webpack-dev-server@^2.9.5:
+  version "2.9.5"
+  resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.9.5.tgz#79336fba0087a66ae491f4869f6545775b18daa8"
   dependencies:
     ansi-html "0.0.7"
     array-includes "^3.0.3"
@@ -7275,12 +7377,13 @@ webpack-dev-server@^2.9.3:
     connect-history-api-fallback "^1.3.0"
     debug "^3.1.0"
     del "^3.0.0"
-    express "^4.13.3"
+    express "^4.16.2"
     html-entities "^1.2.0"
     http-proxy-middleware "~0.17.4"
     import-local "^0.1.1"
     internal-ip "1.2.0"
     ip "^1.1.5"
+    killable "^1.0.0"
     loglevel "^1.4.1"
     opn "^5.1.0"
     portfinder "^1.0.9"
@@ -7301,22 +7404,22 @@ webpack-manifest-plugin@^1.2.1:
     fs-extra "^0.30.0"
     lodash ">=3.5 <5"
 
-webpack-merge@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.0.tgz#6ad72223b3e0b837e531e4597c199f909361511e"
+webpack-merge@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.1.tgz#f1197a0a973e69c6fbeeb6d658219aa8c0c13555"
   dependencies:
     lodash "^4.17.4"
 
 webpack-sources@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
   dependencies:
     source-list-map "^2.0.0"
-    source-map "~0.5.3"
+    source-map "~0.6.1"
 
-webpack@^3.8.1:
-  version "3.8.1"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.8.1.tgz#b16968a81100abe61608b0153c9159ef8bb2bd83"
+webpack@^3.9.1:
+  version "3.9.1"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.9.1.tgz#9a60aa544ed5d4d454c069e3f521aa007e02643c"
   dependencies:
     acorn "^5.0.0"
     acorn-dynamic-import "^2.0.0"
@@ -7349,8 +7452,8 @@ websocket-driver@>=0.5.1:
     websocket-extensions ">=0.1.1"
 
 websocket-extensions@>=0.1.1:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.2.tgz#0e18781de629a18308ce1481650f67ffa2693a5d"
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
 
 websocket.js@^0.1.12:
   version "0.1.12"
@@ -7416,8 +7519,8 @@ wordwrap@~1.0.0:
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
 
 worker-farm@^1.3.1:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.0.tgz#adfdf0cd40581465ed0a1f648f9735722afd5c8d"
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae"
   dependencies:
     errno "^0.1.4"
     xtend "^4.0.1"
@@ -7447,11 +7550,12 @@ write@^0.2.1:
   dependencies:
     mkdirp "^0.5.1"
 
-ws@^2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-2.3.1.tgz#6b94b3e447cb6a363f785eaf94af6359e8e81c80"
+ws@^3.3.1:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.2.tgz#96c1d08b3fefda1d5c1e33700d3bfaa9be2d5608"
   dependencies:
-    safe-buffer "~5.0.1"
+    async-limiter "~1.0.0"
+    safe-buffer "~5.1.0"
     ultron "~1.1.0"
 
 xml-name-validator@^2.0.1: