about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.env.nanobox95
-rw-r--r--.env.production.sample4
-rw-r--r--.env.test1
-rw-r--r--Dockerfile15
-rw-r--r--Gemfile8
-rw-r--r--Gemfile.lock15
-rw-r--r--app/controllers/activitypub/collections_controller.rb57
-rw-r--r--app/controllers/activitypub/outboxes_controller.rb2
-rw-r--r--app/controllers/api/v1/statuses/pins_controller.rb28
-rw-r--r--app/controllers/auth/sessions_controller.rb13
-rw-r--r--app/controllers/concerns/localized.rb12
-rw-r--r--app/controllers/home_controller.rb3
-rw-r--r--app/helpers/instance_helper.rb2
-rw-r--r--app/helpers/stream_entries_helper.rb21
-rw-r--r--app/javascript/mastodon/actions/compose.js69
-rw-r--r--app/javascript/mastodon/actions/dropdown_menu.js10
-rw-r--r--app/javascript/mastodon/actions/store.js13
-rw-r--r--app/javascript/mastodon/actions/timelines.js15
-rw-r--r--app/javascript/mastodon/base_polyfills.js5
-rw-r--r--app/javascript/mastodon/components/attachment_list.js36
-rw-r--r--app/javascript/mastodon/components/autosuggest_textarea.js5
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js55
-rw-r--r--app/javascript/mastodon/components/extended_video_player.js8
-rw-r--r--app/javascript/mastodon/components/media_gallery.js8
-rw-r--r--app/javascript/mastodon/components/permalink.js10
-rw-r--r--app/javascript/mastodon/components/scrollable_list.js12
-rw-r--r--app/javascript/mastodon/components/status.js23
-rw-r--r--app/javascript/mastodon/components/status_content.js11
-rw-r--r--app/javascript/mastodon/components/status_list.js21
-rw-r--r--app/javascript/mastodon/containers/dropdown_menu_container.js19
-rw-r--r--app/javascript/mastodon/features/account/components/header.js18
-rw-r--r--app/javascript/mastodon/features/account_gallery/components/media_item.js32
-rw-r--r--app/javascript/mastodon/features/account_timeline/components/header.js16
-rw-r--r--app/javascript/mastodon/features/account_timeline/index.js19
-rw-r--r--app/javascript/mastodon/features/compose/components/compose_form.js7
-rw-r--r--app/javascript/mastodon/features/compose/containers/compose_form_container.js1
-rw-r--r--app/javascript/mastodon/features/favourited_statuses/index.js4
-rw-r--r--app/javascript/mastodon/features/followers/index.js2
-rw-r--r--app/javascript/mastodon/features/following/index.js2
-rw-r--r--app/javascript/mastodon/features/notifications/components/clear_column_button.js2
-rw-r--r--app/javascript/mastodon/features/notifications/index.js10
-rw-r--r--app/javascript/mastodon/features/report/components/status_check_box.js44
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle_column_error.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle_modal_error.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/focal_point_modal.js4
-rw-r--r--app/javascript/mastodon/features/ui/components/image_loader.js32
-rw-r--r--app/javascript/mastodon/features/ui/components/media_modal.js84
-rw-r--r--app/javascript/mastodon/features/ui/components/tabs_bar.js12
-rw-r--r--app/javascript/mastodon/features/ui/components/video_modal.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/zoomable_image.js151
-rw-r--r--app/javascript/mastodon/features/ui/containers/status_list_container.js5
-rw-r--r--app/javascript/mastodon/features/ui/index.js165
-rw-r--r--app/javascript/mastodon/load_polyfills.js1
-rw-r--r--app/javascript/mastodon/locales/ar.json20
-rw-r--r--app/javascript/mastodon/locales/bg.json6
-rw-r--r--app/javascript/mastodon/locales/ca.json20
-rw-r--r--app/javascript/mastodon/locales/de.json6
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json40
-rw-r--r--app/javascript/mastodon/locales/en.json6
-rw-r--r--app/javascript/mastodon/locales/eo.json26
-rw-r--r--app/javascript/mastodon/locales/es.json6
-rw-r--r--app/javascript/mastodon/locales/fa.json6
-rw-r--r--app/javascript/mastodon/locales/fi.json6
-rw-r--r--app/javascript/mastodon/locales/fr.json22
-rw-r--r--app/javascript/mastodon/locales/gl.json6
-rw-r--r--app/javascript/mastodon/locales/he.json6
-rw-r--r--app/javascript/mastodon/locales/hr.json6
-rw-r--r--app/javascript/mastodon/locales/hu.json6
-rw-r--r--app/javascript/mastodon/locales/hy.json6
-rw-r--r--app/javascript/mastodon/locales/id.json6
-rw-r--r--app/javascript/mastodon/locales/io.json6
-rw-r--r--app/javascript/mastodon/locales/it.json6
-rw-r--r--app/javascript/mastodon/locales/ja.json12
-rw-r--r--app/javascript/mastodon/locales/ko.json6
-rw-r--r--app/javascript/mastodon/locales/nl.json18
-rw-r--r--app/javascript/mastodon/locales/no.json6
-rw-r--r--app/javascript/mastodon/locales/oc.json6
-rw-r--r--app/javascript/mastodon/locales/pl.json6
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json20
-rw-r--r--app/javascript/mastodon/locales/pt.json6
-rw-r--r--app/javascript/mastodon/locales/ru.json6
-rw-r--r--app/javascript/mastodon/locales/sk.json28
-rw-r--r--app/javascript/mastodon/locales/sr-Latn.json6
-rw-r--r--app/javascript/mastodon/locales/sr.json6
-rw-r--r--app/javascript/mastodon/locales/sv.json6
-rw-r--r--app/javascript/mastodon/locales/th.json6
-rw-r--r--app/javascript/mastodon/locales/tr.json6
-rw-r--r--app/javascript/mastodon/locales/uk.json6
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json6
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json6
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json6
-rw-r--r--app/javascript/mastodon/reducers/accounts.js2
-rw-r--r--app/javascript/mastodon/reducers/compose.js22
-rw-r--r--app/javascript/mastodon/reducers/dropdown_menu.js18
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/mastodon/settings.js1
-rw-r--r--app/javascript/styles/mastodon/about.scss43
-rw-r--r--app/javascript/styles/mastodon/admin.scss10
-rw-r--r--app/javascript/styles/mastodon/components.scss272
-rw-r--r--app/javascript/styles/mastodon/forms.scss10
-rw-r--r--app/lib/activitypub/activity.rb4
-rw-r--r--app/lib/activitypub/activity/add.rb13
-rw-r--r--app/lib/activitypub/activity/create.rb15
-rw-r--r--app/lib/activitypub/activity/remove.rb14
-rw-r--r--app/lib/activitypub/adapter.rb1
-rw-r--r--app/lib/formatter.rb2
-rw-r--r--app/lib/ostatus/activity/creation.rb13
-rw-r--r--app/models/account.rb1
-rw-r--r--app/models/concerns/account_avatar.rb4
-rw-r--r--app/models/concerns/omniauthable.rb3
-rw-r--r--app/models/concerns/remotable.rb6
-rw-r--r--app/models/status.rb8
-rw-r--r--app/models/user.rb2
-rw-r--r--app/serializers/activitypub/actor_serializer.rb6
-rw-r--r--app/serializers/activitypub/add_serializer.rb24
-rw-r--r--app/serializers/activitypub/collection_serializer.rb2
-rw-r--r--app/serializers/activitypub/note_serializer.rb9
-rw-r--r--app/serializers/activitypub/outbox_serializer.rb8
-rw-r--r--app/serializers/activitypub/remove_serializer.rb24
-rw-r--r--app/serializers/rest/instance_serializer.rb2
-rw-r--r--app/services/activitypub/fetch_featured_collection_service.rb52
-rw-r--r--app/services/activitypub/process_account_service.rb22
-rw-r--r--app/services/block_domain_service.rb37
-rw-r--r--app/services/post_status_service.rb10
-rw-r--r--app/services/search_service.rb4
-rw-r--r--app/views/about/_forms.html.haml11
-rw-r--r--app/views/about/show.html.haml7
-rw-r--r--app/views/accounts/_og.html.haml2
-rw-r--r--app/views/accounts/show.html.haml13
-rw-r--r--app/views/auth/passwords/edit.html.haml2
-rw-r--r--app/views/auth/registrations/_sessions.html.haml2
-rw-r--r--app/views/auth/registrations/edit.html.haml8
-rw-r--r--app/views/follower_accounts/index.html.haml4
-rw-r--r--app/views/following_accounts/index.html.haml4
-rwxr-xr-xapp/views/layouts/application.html.haml8
-rw-r--r--app/views/stream_entries/show.html.haml2
-rw-r--r--app/workers/activitypub/synchronize_featured_collection_worker.rb13
-rw-r--r--boxfile.yml23
-rw-r--r--config/application.rb9
-rw-r--r--config/environments/development.rb2
-rw-r--r--config/environments/production.rb2
-rw-r--r--config/environments/test.rb5
-rw-r--r--config/initializers/omniauth.rb2
-rw-r--r--config/locales/ar.yml11
-rw-r--r--config/locales/bg.yml2
-rw-r--r--config/locales/ca.yml20
-rw-r--r--config/locales/de.yml2
-rw-r--r--config/locales/devise.ar.yml5
-rw-r--r--config/locales/devise.sk.yml8
-rw-r--r--config/locales/doorkeeper.sk.yml4
-rw-r--r--config/locales/en.yml4
-rw-r--r--config/locales/eo.yml17
-rw-r--r--config/locales/es.yml2
-rw-r--r--config/locales/fa.yml2
-rw-r--r--config/locales/fi.yml2
-rw-r--r--config/locales/fr.yml29
-rw-r--r--config/locales/gl.yml20
-rw-r--r--config/locales/he.yml2
-rw-r--r--config/locales/hr.yml2
-rw-r--r--config/locales/hu.yml2
-rw-r--r--config/locales/id.yml2
-rw-r--r--config/locales/io.yml2
-rw-r--r--config/locales/it.yml2
-rw-r--r--config/locales/ja.yml11
-rw-r--r--config/locales/ko.yml23
-rw-r--r--config/locales/nl.yml22
-rw-r--r--config/locales/no.yml2
-rw-r--r--config/locales/oc.yml2
-rw-r--r--config/locales/pl.yml5
-rw-r--r--config/locales/pt-BR.yml25
-rw-r--r--config/locales/pt.yml2
-rw-r--r--config/locales/ru.yml2
-rw-r--r--config/locales/simple_form.ar.yml6
-rw-r--r--config/locales/simple_form.eo.yml2
-rw-r--r--config/locales/simple_form.pt-BR.yml2
-rw-r--r--config/locales/simple_form.sk.yml16
-rw-r--r--config/locales/sk.yml115
-rw-r--r--config/locales/sr-Latn.yml2
-rw-r--r--config/locales/sr.yml2
-rw-r--r--config/locales/sv.yml2
-rw-r--r--config/locales/th.yml2
-rw-r--r--config/locales/tr.yml2
-rw-r--r--config/locales/uk.yml2
-rw-r--r--config/locales/zh-CN.yml2
-rw-r--r--config/locales/zh-HK.yml2
-rw-r--r--config/locales/zh-TW.yml2
-rw-r--r--config/navigation.rb2
-rw-r--r--config/routes.rb2
-rw-r--r--db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb5
-rw-r--r--db/schema.rb3
-rw-r--r--docker-compose.yml8
-rw-r--r--lib/mastodon/version.rb2
-rw-r--r--lib/paperclip/lazy_thumbnail.rb4
-rw-r--r--lib/tasks/assets.rake6
-rw-r--r--lib/tasks/mastodon.rake8
-rw-r--r--nanobox/nginx-web.conf.erb8
-rw-r--r--package.json7
-rw-r--r--spec/controllers/concerns/localized_spec.rb53
-rw-r--r--spec/controllers/home_controller_spec.rb15
-rw-r--r--spec/helpers/instance_helper_spec.rb6
-rw-r--r--spec/lib/activitypub/activity/add_spec.rb29
-rw-r--r--spec/lib/activitypub/activity/create_spec.rb29
-rw-r--r--spec/lib/activitypub/activity/remove_spec.rb30
-rw-r--r--spec/lib/activitypub/activity/update_spec.rb1
-rw-r--r--yarn.lock131
206 files changed, 2393 insertions, 729 deletions
diff --git a/.env.nanobox b/.env.nanobox
index 48204a6bf..0d14f8a00 100644
--- a/.env.nanobox
+++ b/.env.nanobox
@@ -13,11 +13,29 @@ DB_PORT=5432
 
 DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
 
+# Optional ElasticSearch configuration
+# ES_ENABLED=true
+# ES_HOST=localhost
+# ES_PORT=9200
+
+# Optimizations
+LD_PRELOAD=/data/lib/libjemalloc.so
+
+# ImageMagick optimizations
+MAGICK_TEMPORARY_PATH=/app/tmp
+MAGICK_MEMORY_LIMIT=128MiB
+MAGICK_MAP_LIMIT=64MiB
+MAGICK_TIME_LIMIT=15
+MAGICK_AREA_LIMIT=16MP
+MAGICK_WIDTH_LIMIT=8KP
+MAGICK_HEIGHT_LIMIT=8KP
+
 # Federation
-# Note: Changing LOCAL_DOMAIN or LOCAL_HTTPS at a later time will cause unwanted side effects.
+# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
 # LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
 LOCAL_DOMAIN=${APP_NAME}.nanoapp.io
-LOCAL_HTTPS=false
+
+# Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links)
 
 # Use this only if you need to run mastodon on a different domain than the one used for federation.
 # You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md
@@ -31,7 +49,6 @@ LOCAL_HTTPS=false
 
 # Application secrets
 # Generate each with the `rake secret` task (`nanobox run bundle exec rake secret`)
-PAPERCLIP_SECRET=$PAPERCLIP_SECRET
 SECRET_KEY_BASE=$SECRET_KEY_BASE
 OTP_SECRET=$OTP_SECRET
 
@@ -131,9 +148,79 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
 
 # Cluster number setting for streaming API server.
 # If you comment out following line, cluster number will be `numOfCpuCores - 1`.
-STREAMING_CLUSTER_NUM=1
+# STREAMING_CLUSTER_NUM=1
 
 # Docker mastodon user
 # If you use Docker, you may want to assign UID/GID manually.
 # UID=1000
 # GID=1000
+
+# LDAP authentication (optional)
+# LDAP_ENABLED=true
+# LDAP_HOST=localhost
+# LDAP_PORT=389
+# LDAP_METHOD=simple_tls
+# LDAP_BASE=
+# LDAP_BIND_DN=
+# LDAP_PASSWORD=
+# LDAP_UID=cn
+
+# PAM authentication (optional)
+# PAM authentication uses for the email generation the "email" pam variable
+# and optional as fallback PAM_DEFAULT_SUFFIX
+# The pam environment variable "email" is provided by:
+# https://github.com/devkral/pam_email_extractor
+# PAM_ENABLED=true
+# Fallback Suffix for email address generation (nil by default)
+# PAM_DEFAULT_SUFFIX=pam
+# Name of the pam service (pam "auth" section is evaluated)
+# PAM_DEFAULT_SERVICE=rpam
+# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
+# PAM_CONTROLLED_SERVICE=rpam
+
+# Global OAuth settings (optional) :
+# If you have only one strategy, you may want to enable this
+# OAUTH_REDIRECT_AT_SIGN_IN=true
+
+# Optional CAS authentication (cf. omniauth-cas) :
+# CAS_ENABLED=true
+# CAS_URL=https://sso.myserver.com/
+# CAS_HOST=sso.myserver.com/
+# CAS_PORT=443
+# CAS_SSL=true
+# CAS_VALIDATE_URL=
+# CAS_CALLBACK_URL=
+# CAS_LOGOUT_URL=
+# CAS_LOGIN_URL=
+# CAS_UID_FIELD='user'
+# CAS_CA_PATH=
+# CAS_DISABLE_SSL_VERIFICATION=false
+# CAS_UID_KEY='user'
+# CAS_NAME_KEY='name'
+# CAS_EMAIL_KEY='email'
+# CAS_NICKNAME_KEY='nickname'
+# CAS_FIRST_NAME_KEY='firstname'
+# CAS_LAST_NAME_KEY='lastname'
+# CAS_LOCATION_KEY='location'
+# CAS_IMAGE_KEY='image'
+# CAS_PHONE_KEY='phone'
+
+# Optional SAML authentication (cf. omniauth-saml)
+# SAML_ENABLED=true
+# SAML_ACS_URL=
+# SAML_ISSUER=http://localhost:3000/auth/auth/saml/callback
+# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
+# SAML_IDP_CERT=
+# SAML_IDP_CERT_FINGERPRINT=
+# SAML_NAME_IDENTIFIER_FORMAT=
+# SAML_CERT=
+# SAML_PRIVATE_KEY=
+# SAML_SECURITY_WANT_ASSERTION_SIGNED=true
+# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true
+# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
+# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1"
+# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
+# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.5.4.42"
+# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
+# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
+# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
diff --git a/.env.production.sample b/.env.production.sample
index d920f18e9..f169e9b35 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -207,7 +207,9 @@ STREAMING_CLUSTER_NUM=1
 # SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
 # SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1"
 # SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
-# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.5.4.42"
+# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.16.840.1.113730.3.1.241"
+# SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME="urn:oid:2.5.4.42"
+# SAML_ATTRIBUTES_STATEMENTS_LAST_NAME="urn:oid:2.5.4.4"
 # SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
 # SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
 # SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
diff --git a/.env.test b/.env.test
index e25c040ac..b57f52e30 100644
--- a/.env.test
+++ b/.env.test
@@ -1,4 +1,3 @@
 # Federation
 LOCAL_DOMAIN=cb6e6126.ngrok.io
 LOCAL_HTTPS=true
-OTP_SECRET=100c7faeef00caa29242f6b04156742bf76065771fd4117990c4282b8748ff3d99f8fdae97c982ab5bd2e6756a159121377cce4421f4a8ecd2d67bd7749a3fb4
diff --git a/Dockerfile b/Dockerfile
index a50122057..73051cd4b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
 FROM ruby:2.5.0-alpine3.7
 
 LABEL maintainer="https://github.com/tootsuite/mastodon" \
-      description="A GNU Social-compatible microblogging server"
+      description="Your self-hosted, globally interconnected microblogging community"
 
 ARG UID=991
 ARG GID=991
@@ -9,8 +9,8 @@ ARG GID=991
 ENV RAILS_SERVE_STATIC_FILES=true \
     RAILS_ENV=production NODE_ENV=production
 
-ARG YARN_VERSION=1.3.2
-ARG YARN_DOWNLOAD_SHA256=6cfe82e530ef0837212f13e45c1565ba53f5199eec2527b85ecbcd88bf26821d
+ARG YARN_VERSION=1.5.1
+ARG YARN_DOWNLOAD_SHA256=cd31657232cf48d57fdbff55f38bfa058d2fb4950450bd34af72dac796af4de1
 ARG LIBICONV_VERSION=1.15
 ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
 
@@ -38,7 +38,6 @@ RUN apk -U upgrade \
     libidn \
     libpq \
     nodejs \
-    nodejs-npm \
     protobuf \
     su-exec \
     tini \
@@ -73,9 +72,13 @@ RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-in
  && yarn --pure-lockfile \
  && yarn cache clean
 
-RUN addgroup -g ${GID} mastodon && adduser -h /mastodon -s /bin/sh -D -G mastodon -u ${UID} mastodon
+RUN addgroup -g ${GID} mastodon && adduser -h /mastodon -s /bin/sh -D -G mastodon -u ${UID} mastodon \
+ && mkdir -p /mastodon/public/system /mastodon/public/assets /mastodon/public/packs \
+ && chown -R mastodon:mastodon /mastodon/public
 
-COPY --chown=mastodon:mastodon . /mastodon
+COPY . /mastodon
+
+RUN chown -R mastodon:mastodon /mastodon
 
 VOLUME /mastodon/public/system /mastodon/public/assets /mastodon/public/packs
 
diff --git a/Gemfile b/Gemfile
index bf53d4c77..22192db4d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -28,15 +28,15 @@ gem 'bootsnap'
 gem 'browser'
 gem 'charlock_holmes', '~> 0.7.5'
 gem 'iso-639'
-gem 'chewy', '~> 0.10', git: 'https://github.com/toptal/chewy.git'
+gem 'chewy', '~> 5.0'
 gem 'cld3', '~> 3.2.0'
 gem 'devise', '~> 4.4'
 gem 'devise-two-factor', '~> 3.0'
 
 gem 'devise_pam_authenticatable2', '~> 8.0', install_if: -> { ENV['PAM_ENABLED'] == 'true' }
-gem 'net-ldap', '~> 0.10', install_if: -> { ENV['LDAP_ENABLED'] == 'true' }
-gem 'omniauth-cas', '~> 1.1', install_if: -> { ENV['CAS_ENABLED'] == 'true' }
-gem 'omniauth-saml', '~> 1.10', install_if: -> { ENV['SAML_ENABLED'] == 'true' }
+gem 'net-ldap', '~> 0.10'
+gem 'omniauth-cas', '~> 1.1'
+gem 'omniauth-saml', '~> 1.10'
 gem 'omniauth', '~> 1.2'
 
 gem 'doorkeeper', '~> 4.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index f0ad8ab55..693773d36 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,12 +1,3 @@
-GIT
-  remote: https://github.com/toptal/chewy.git
-  revision: a7d21eb4b0bd7415533ef134bb6d31b2df309701
-  specs:
-    chewy (0.10.1)
-      activesupport (>= 4.0)
-      elasticsearch (>= 2.0.0)
-      elasticsearch-dsl
-
 GEM
   remote: https://rubygems.org/
   specs:
@@ -118,6 +109,10 @@ GEM
     case_transform (0.2)
       activesupport
     charlock_holmes (0.7.5)
+    chewy (5.0.0)
+      activesupport (>= 4.0)
+      elasticsearch (>= 2.0.0)
+      elasticsearch-dsl
     chunky_png (1.3.8)
     cld3 (3.2.2)
       ffi (>= 1.1.0, < 1.10.0)
@@ -634,7 +629,7 @@ DEPENDENCIES
   capistrano-yarn (~> 2.0)
   capybara (~> 2.15)
   charlock_holmes (~> 0.7.5)
-  chewy (~> 0.10)!
+  chewy (~> 5.0)
   cld3 (~> 3.2.0)
   climate_control (~> 0.2)
   devise (~> 4.4)
diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb
new file mode 100644
index 000000000..081914016
--- /dev/null
+++ b/app/controllers/activitypub/collections_controller.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+class ActivityPub::CollectionsController < Api::BaseController
+  include SignatureVerification
+
+  before_action :set_account
+  before_action :set_size
+  before_action :set_statuses
+
+  def show
+    render json: collection_presenter,
+           serializer: ActivityPub::CollectionSerializer,
+           adapter: ActivityPub::Adapter,
+           content_type: 'application/activity+json',
+           skip_activities: true
+  end
+
+  private
+
+  def set_account
+    @account = Account.find_local!(params[:account_username])
+  end
+
+  def set_statuses
+    @statuses = scope_for_collection.paginate_by_max_id(20, params[:max_id], params[:since_id])
+    @statuses = cache_collection(@statuses, Status)
+  end
+
+  def set_size
+    case params[:id]
+    when 'featured'
+      @account.pinned_statuses.count
+    else
+      raise ActiveRecord::NotFound
+    end
+  end
+
+  def scope_for_collection
+    case params[:id]
+    when 'featured'
+      @account.statuses.permitted_for(@account, signed_request_account).tap do |scope|
+        scope.merge!(@account.pinned_statuses)
+      end
+    else
+      raise ActiveRecord::NotFound
+    end
+  end
+
+  def collection_presenter
+    ActivityPub::CollectionPresenter.new(
+      id: account_collection_url(@account, params[:id]),
+      type: :ordered,
+      size: @size,
+      items: @statuses
+    )
+  end
+end
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index a431e3557..9ed700c1e 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -9,7 +9,7 @@ class ActivityPub::OutboxesController < Api::BaseController
     @statuses = @account.statuses.permitted_for(@account, signed_request_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
     @statuses = cache_collection(@statuses, Status)
 
-    render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
+    render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
   end
 
   private
diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb
index 3de1009b8..bba6a6f48 100644
--- a/app/controllers/api/v1/statuses/pins_controller.rb
+++ b/app/controllers/api/v1/statuses/pins_controller.rb
@@ -11,12 +11,18 @@ class Api::V1::Statuses::PinsController < Api::BaseController
 
   def create
     StatusPin.create!(account: current_account, status: @status)
+    distribute_add_activity!
     render json: @status, serializer: REST::StatusSerializer
   end
 
   def destroy
     pin = StatusPin.find_by(account: current_account, status: @status)
-    pin&.destroy!
+
+    if pin
+      pin.destroy!
+      distribute_remove_activity!
+    end
+
     render json: @status, serializer: REST::StatusSerializer
   end
 
@@ -25,4 +31,24 @@ class Api::V1::Statuses::PinsController < Api::BaseController
   def set_status
     @status = Status.find(params[:status_id])
   end
+
+  def distribute_add_activity!
+    json = ActiveModelSerializers::SerializableResource.new(
+      @status,
+      serializer: ActivityPub::AddSerializer,
+      adapter: ActivityPub::Adapter
+    ).as_json
+
+    ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account)
+  end
+
+  def distribute_remove_activity!
+    json = ActiveModelSerializers::SerializableResource.new(
+      @status,
+      serializer: ActivityPub::RemoveSerializer,
+      adapter: ActivityPub::Adapter
+    ).as_json
+
+    ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account)
+  end
 end
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index c9e507343..62f3b2eb6 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -13,10 +13,9 @@ class Auth::SessionsController < Devise::SessionsController
 
   def new
     Devise.omniauth_configs.each do |provider, config|
-      if config.strategy.redirect_at_sign_in
-        return redirect_to(omniauth_authorize_path(resource_name, provider))
-      end
+      return redirect_to(omniauth_authorize_path(resource_name, provider)) if config.strategy.redirect_at_sign_in
     end
+
     super
   end
 
@@ -60,6 +59,14 @@ class Auth::SessionsController < Devise::SessionsController
     end
   end
 
+  def after_sign_out_path_for(_resource_or_scope)
+    Devise.omniauth_configs.each_value do |config|
+      return root_path if config.strategy.redirect_at_sign_in
+    end
+
+    super
+  end
+
   def two_factor_enabled?
     find_user.try(:otp_required_for_login?)
   end
diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb
index a9ea60f7d..e697284a8 100644
--- a/app/controllers/concerns/localized.rb
+++ b/app/controllers/concerns/localized.rb
@@ -17,11 +17,7 @@ module Localized
   end
 
   def default_locale
-    request_locale || env_locale || I18n.default_locale
-  end
-
-  def env_locale
-    ENV['DEFAULT_LOCALE']
+    request_locale || I18n.default_locale
   end
 
   def request_locale
@@ -29,12 +25,10 @@ module Localized
   end
 
   def preferred_locale
-    http_accept_language.preferred_language_from([env_locale]) ||
-      http_accept_language.preferred_language_from(I18n.available_locales)
+    http_accept_language.preferred_language_from(I18n.available_locales)
   end
 
   def compatible_locale
-    http_accept_language.compatible_language_from([env_locale]) ||
-      http_accept_language.compatible_language_from(I18n.available_locales)
+    http_accept_language.compatible_language_from(I18n.available_locales)
   end
 end
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 7437a647e..a8ec0dcc9 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -35,7 +35,8 @@ class HomeController < ApplicationController
       end
     end
 
-    redirect_to(default_redirect_path)
+    matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z})
+    redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
   end
 
   def set_pack
diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb
index 22a19c52b..dd0b25f3e 100644
--- a/app/helpers/instance_helper.rb
+++ b/app/helpers/instance_helper.rb
@@ -2,7 +2,7 @@
 
 module InstanceHelper
   def site_title
-    Setting.site_title.presence || site_hostname
+    Setting.site_title
   end
 
   def site_hostname
diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb
index 445114985..54b92bdf4 100644
--- a/app/helpers/stream_entries_helper.rb
+++ b/app/helpers/stream_entries_helper.rb
@@ -8,6 +8,27 @@ module StreamEntriesHelper
     account.display_name.presence || account.username
   end
 
+  def account_description(account)
+    prepend_str = [
+      [
+        number_to_human(account.statuses_count, strip_insignificant_zeros: true),
+        t('accounts.posts'),
+      ].join(' '),
+
+      [
+        number_to_human(account.following_count, strip_insignificant_zeros: true),
+        t('accounts.following'),
+      ].join(' '),
+
+      [
+        number_to_human(account.followers_count, strip_insignificant_zeros: true),
+        t('accounts.followers'),
+      ].join(' '),
+    ].join(', ')
+
+    [prepend_str, account.note].join(' · ')
+  end
+
   def stream_link_target
     embedded_view? ? '_blank' : nil
   end
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 1732ff189..130b4af23 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -1,6 +1,7 @@
 import api from '../api';
 import { throttle } from 'lodash';
 import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
+import { tagHistory } from '../settings';
 import { useEmoji } from './emojis';
 
 import {
@@ -27,6 +28,9 @@ export const COMPOSE_UPLOAD_UNDO     = 'COMPOSE_UPLOAD_UNDO';
 export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
 export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
 export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
+export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
+
+export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
 
 export const COMPOSE_MOUNT   = 'COMPOSE_MOUNT';
 export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
@@ -92,8 +96,9 @@ export function mentionCompose(account, router) {
 export function submitCompose() {
   return function (dispatch, getState) {
     const status = getState().getIn(['compose', 'text'], '');
+    const media  = getState().getIn(['compose', 'media_attachments']);
 
-    if (!status || !status.length) {
+    if ((!status || !status.length) && media.size === 0) {
       return;
     }
 
@@ -102,7 +107,7 @@ export function submitCompose() {
     api(getState).post('/api/v1/statuses', {
       status,
       in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
-      media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
+      media_ids: media.map(item => item.get('id')),
       sensitive: getState().getIn(['compose', 'sensitive']),
       spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
       visibility: getState().getIn(['compose', 'privacy']),
@@ -111,6 +116,7 @@ export function submitCompose() {
         'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
       },
     }).then(function (response) {
+      dispatch(insertIntoTagHistory(response.data.tags));
       dispatch(submitComposeSuccess({ ...response.data }));
 
       // To make the app more responsive, immediately get the status into the columns
@@ -273,12 +279,22 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
   dispatch(readyComposeSuggestionsEmojis(token, results));
 };
 
+const fetchComposeSuggestionsTags = (dispatch, getState, token) => {
+  dispatch(updateSuggestionTags(token));
+};
+
 export function fetchComposeSuggestions(token) {
   return (dispatch, getState) => {
-    if (token[0] === ':') {
+    switch (token[0]) {
+    case ':':
       fetchComposeSuggestionsEmojis(dispatch, getState, token);
-    } else {
+      break;
+    case '#':
+      fetchComposeSuggestionsTags(dispatch, getState, token);
+      break;
+    default:
       fetchComposeSuggestionsAccounts(dispatch, getState, token);
+      break;
     }
   };
 };
@@ -308,6 +324,9 @@ export function selectComposeSuggestion(position, token, suggestion) {
       startPosition = position - 1;
 
       dispatch(useEmoji(suggestion));
+    } else if (suggestion[0] === '#') {
+      completion    = suggestion;
+      startPosition = position - 1;
     } else {
       completion    = getState().getIn(['accounts', suggestion, 'acct']);
       startPosition = position;
@@ -322,6 +341,48 @@ export function selectComposeSuggestion(position, token, suggestion) {
   };
 };
 
+export function updateSuggestionTags(token) {
+  return {
+    type: COMPOSE_SUGGESTION_TAGS_UPDATE,
+    token,
+  };
+}
+
+export function updateTagHistory(tags) {
+  return {
+    type: COMPOSE_TAG_HISTORY_UPDATE,
+    tags,
+  };
+}
+
+export function hydrateCompose() {
+  return (dispatch, getState) => {
+    const me = getState().getIn(['meta', 'me']);
+    const history = tagHistory.get(me);
+
+    if (history !== null) {
+      dispatch(updateTagHistory(history));
+    }
+  };
+}
+
+function insertIntoTagHistory(tags) {
+  return (dispatch, getState) => {
+    const state = getState();
+    const oldHistory = state.getIn(['compose', 'tagHistory']);
+    const me = state.getIn(['meta', 'me']);
+    const names = tags.map(({ name }) => name);
+    const intersectedOldHistory = oldHistory.filter(name => !names.includes(name));
+
+    names.push(...intersectedOldHistory.toJS());
+
+    const newHistory = names.slice(0, 1000);
+
+    tagHistory.set(me, newHistory);
+    dispatch(updateTagHistory(newHistory));
+  };
+}
+
 export function mountCompose() {
   return {
     type: COMPOSE_MOUNT,
diff --git a/app/javascript/mastodon/actions/dropdown_menu.js b/app/javascript/mastodon/actions/dropdown_menu.js
new file mode 100644
index 000000000..217ba4e74
--- /dev/null
+++ b/app/javascript/mastodon/actions/dropdown_menu.js
@@ -0,0 +1,10 @@
+export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
+export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
+
+export function openDropdownMenu(id, placement) {
+  return { type: DROPDOWN_MENU_OPEN, id, placement };
+}
+
+export function closeDropdownMenu(id) {
+  return { type: DROPDOWN_MENU_CLOSE, id };
+}
diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js
index a1db0fdd5..2dd94a998 100644
--- a/app/javascript/mastodon/actions/store.js
+++ b/app/javascript/mastodon/actions/store.js
@@ -1,4 +1,5 @@
 import { Iterable, fromJS } from 'immutable';
+import { hydrateCompose } from './compose';
 
 export const STORE_HYDRATE = 'STORE_HYDRATE';
 export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
@@ -8,10 +9,14 @@ const convertState = rawState =>
     Iterable.isIndexed(v) ? v.toList() : v.toMap());
 
 export function hydrateStore(rawState) {
-  const state = convertState(rawState);
+  return dispatch => {
+    const state = convertState(rawState);
 
-  return {
-    type: STORE_HYDRATE,
-    state,
+    dispatch({
+      type: STORE_HYDRATE,
+      state,
+    });
+
+    dispatch(hydrateCompose());
   };
 };
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 858a12b15..f0ab16a2d 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -117,13 +117,14 @@ export function refreshTimeline(timelineId, path, params = {}) {
   };
 };
 
-export const refreshHomeTimeline         = () => refreshTimeline('home', '/api/v1/timelines/home');
-export const refreshPublicTimeline       = () => refreshTimeline('public', '/api/v1/timelines/public');
-export const refreshCommunityTimeline    = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
-export const refreshAccountTimeline      = (accountId, withReplies) => refreshTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
-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 const refreshHomeTimeline            = () => refreshTimeline('home', '/api/v1/timelines/home');
+export const refreshPublicTimeline          = () => refreshTimeline('public', '/api/v1/timelines/public');
+export const refreshCommunityTimeline       = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
+export const refreshAccountTimeline         = (accountId, withReplies) => refreshTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
+export const refreshAccountFeaturedTimeline = accountId => refreshTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
+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 {
diff --git a/app/javascript/mastodon/base_polyfills.js b/app/javascript/mastodon/base_polyfills.js
index 7856b26f9..8fbb17785 100644
--- a/app/javascript/mastodon/base_polyfills.js
+++ b/app/javascript/mastodon/base_polyfills.js
@@ -3,6 +3,7 @@ import 'intl/locale-data/jsonp/en';
 import 'es6-symbol/implement';
 import includes from 'array-includes';
 import assign from 'object-assign';
+import values from 'object.values';
 import isNaN from 'is-nan';
 
 if (!Array.prototype.includes) {
@@ -13,6 +14,10 @@ if (!Object.assign) {
   Object.assign = assign;
 }
 
+if (!Object.values) {
+  values.shim();
+}
+
 if (!Number.isNaN) {
   Number.isNaN = isNaN;
 }
diff --git a/app/javascript/mastodon/components/attachment_list.js b/app/javascript/mastodon/components/attachment_list.js
index 9f2d46ddd..8e5bb0e0b 100644
--- a/app/javascript/mastodon/components/attachment_list.js
+++ b/app/javascript/mastodon/components/attachment_list.js
@@ -1,5 +1,6 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
@@ -8,10 +9,29 @@ export default class AttachmentList extends ImmutablePureComponent {
 
   static propTypes = {
     media: ImmutablePropTypes.list.isRequired,
+    compact: PropTypes.bool,
   };
 
   render () {
-    const { media } = this.props;
+    const { media, compact } = this.props;
+
+    if (compact) {
+      return (
+        <div className='attachment-list compact'>
+          <ul className='attachment-list__list'>
+            {media.map(attachment => {
+              const displayUrl = attachment.get('remote_url') || attachment.get('url');
+
+              return (
+                <li key={attachment.get('id')}>
+                  <a href={displayUrl} target='_blank' rel='noopener'><i className='fa fa-link' /> {filename(displayUrl)}</a>
+                </li>
+              );
+            })}
+          </ul>
+        </div>
+      );
+    }
 
     return (
       <div className='attachment-list'>
@@ -20,11 +40,15 @@ export default class AttachmentList extends ImmutablePureComponent {
         </div>
 
         <ul className='attachment-list__list'>
-          {media.map(attachment => (
-            <li key={attachment.get('id')}>
-              <a href={attachment.get('remote_url')} target='_blank' rel='noopener'>{filename(attachment.get('remote_url'))}</a>
-            </li>
-          ))}
+          {media.map(attachment => {
+            const displayUrl = attachment.get('remote_url') || attachment.get('url');
+
+            return (
+              <li key={attachment.get('id')}>
+                <a href={displayUrl} target='_blank' rel='noopener'>{filename(displayUrl)}</a>
+              </li>
+            );
+          })}
         </ul>
       </div>
     );
diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js
index 6a16e2fc7..34904194f 100644
--- a/app/javascript/mastodon/components/autosuggest_textarea.js
+++ b/app/javascript/mastodon/components/autosuggest_textarea.js
@@ -20,7 +20,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
     word = str.slice(left, right + caretPosition);
   }
 
-  if (!word || word.trim().length < 3 || ['@', ':'].indexOf(word[0]) === -1) {
+  if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
     return [null, null];
   }
 
@@ -170,6 +170,9 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
     if (typeof suggestion === 'object') {
       inner = <AutosuggestEmoji emoji={suggestion} />;
       key   = suggestion.id;
+    } else if (suggestion[0] === '#') {
+      inner = suggestion;
+      key   = suggestion;
     } else {
       inner = <AutosuggestAccountContainer id={suggestion} />;
       key   = suggestion;
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index 43dc0d6e3..c5c6f73b3 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -8,6 +8,7 @@ import spring from 'react-motion/lib/spring';
 import detectPassiveEvents from 'detect-passive-events';
 
 const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
+let id = 0;
 
 class DropdownMenu extends React.PureComponent {
 
@@ -29,6 +30,10 @@ class DropdownMenu extends React.PureComponent {
     placement: 'bottom',
   };
 
+  state = {
+    mounted: false,
+  };
+
   handleDocumentClick = e => {
     if (this.node && !this.node.contains(e.target)) {
       this.props.onClose();
@@ -38,6 +43,7 @@ class DropdownMenu extends React.PureComponent {
   componentDidMount () {
     document.addEventListener('click', this.handleDocumentClick, false);
     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
+    this.setState({ mounted: true });
   }
 
   componentWillUnmount () {
@@ -82,11 +88,15 @@ class DropdownMenu extends React.PureComponent {
 
   render () {
     const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
+    const { mounted } = this.state;
 
     return (
       <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
         {({ opacity, scaleX, scaleY }) => (
-          <div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}>
+          // It should not be transformed when mounting because the resulting
+          // size will be used to determine the coordinate of the menu by
+          // react-overlays
+          <div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
             <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
 
             <ul>
@@ -115,8 +125,10 @@ export default class Dropdown extends React.PureComponent {
     status: ImmutablePropTypes.map,
     isUserTouching: PropTypes.func,
     isModalOpen: PropTypes.bool.isRequired,
-    onModalOpen: PropTypes.func,
-    onModalClose: PropTypes.func,
+    onOpen: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    dropdownPlacement: PropTypes.string,
+    openDropdownId: PropTypes.number,
   };
 
   static defaultProps = {
@@ -124,37 +136,28 @@ export default class Dropdown extends React.PureComponent {
   };
 
   state = {
-    expanded: false,
+    id: id++,
   };
 
-  handleClick = () => {
-    if (!this.state.expanded && this.props.isUserTouching() && this.props.onModalOpen) {
-      const { status, items } = this.props;
-
-      this.props.onModalOpen({
-        status,
-        actions: items,
-        onClick: this.handleItemClick,
-      });
+  handleClick = ({ target }) => {
+    if (this.state.id === this.props.openDropdownId) {
+      this.handleClose();
+    } else {
+      const { top } = target.getBoundingClientRect();
+      const placement = top * 2 < innerHeight ? 'bottom' : 'top';
 
-      return;
+      this.props.onOpen(this.state.id, this.handleItemClick, placement);
     }
-
-    this.setState({ expanded: !this.state.expanded });
   }
 
   handleClose = () => {
-    if (this.props.onModalClose) {
-      this.props.onModalClose();
-    }
-
-    this.setState({ expanded: false });
+    this.props.onClose(this.state.id);
   }
 
   handleKeyDown = e => {
     switch(e.key) {
     case 'Enter':
-      this.handleClick();
+      this.handleClick(e);
       break;
     case 'Escape':
       this.handleClose();
@@ -186,22 +189,22 @@ export default class Dropdown extends React.PureComponent {
   }
 
   render () {
-    const { icon, items, size, title, disabled } = this.props;
-    const { expanded } = this.state;
+    const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId } = this.props;
+    const open = this.state.id === openDropdownId;
 
     return (
       <div onKeyDown={this.handleKeyDown}>
         <IconButton
           icon={icon}
           title={title}
-          active={expanded}
+          active={open}
           disabled={disabled}
           size={size}
           ref={this.setTargetRef}
           onClick={this.handleClick}
         />
 
-        <Overlay show={expanded} placement='bottom' target={this.findTarget}>
+        <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
           <DropdownMenu items={items} onClose={this.handleClose} />
         </Overlay>
       </div>
diff --git a/app/javascript/mastodon/components/extended_video_player.js b/app/javascript/mastodon/components/extended_video_player.js
index f8bd067e8..9e2f6835a 100644
--- a/app/javascript/mastodon/components/extended_video_player.js
+++ b/app/javascript/mastodon/components/extended_video_player.js
@@ -11,6 +11,7 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
     time: PropTypes.number,
     controls: PropTypes.bool.isRequired,
     muted: PropTypes.bool.isRequired,
+    onClick: PropTypes.func,
   };
 
   handleLoadedData = () => {
@@ -31,6 +32,12 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
     this.video = c;
   }
 
+  handleClick = e => {
+    e.stopPropagation();
+    const handler = this.props.onClick;
+    if (handler) handler();
+  }
+
   render () {
     const { src, muted, controls, alt } = this.props;
 
@@ -46,6 +53,7 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
           muted={muted}
           controls={controls}
           loop={!controls}
+          onClick={this.handleClick}
         />
       </div>
     );
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index 3568a8440..71436500a 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -167,6 +167,14 @@ class Item extends React.PureComponent {
           vShift = shiftToPoint(widthRatio, (containerHeight * (height / 100)), originalHeight, focusY, true);
         }
 
+        if (originalWidth > originalHeight) {
+          imageStyle.height = '100%';
+          imageStyle.width  = 'auto';
+        } else {
+          imageStyle.height = 'auto';
+          imageStyle.width  = '100%';
+        }
+
         imageStyle.top  = vShift;
         imageStyle.left = hShift;
       } else {
diff --git a/app/javascript/mastodon/components/permalink.js b/app/javascript/mastodon/components/permalink.js
index d726d37a2..b369e9812 100644
--- a/app/javascript/mastodon/components/permalink.js
+++ b/app/javascript/mastodon/components/permalink.js
@@ -12,9 +12,15 @@ export default class Permalink extends React.PureComponent {
     href: PropTypes.string.isRequired,
     to: PropTypes.string.isRequired,
     children: PropTypes.node,
+    onInterceptClick: PropTypes.func,
   };
 
-  handleClick = (e) => {
+  handleClick = e => {
+    if (this.props.onInterceptClick && this.props.onInterceptClick()) {
+      e.preventDefault();
+      return;
+    }
+
     if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
       this.context.router.history.push(this.props.to);
@@ -22,7 +28,7 @@ export default class Permalink extends React.PureComponent {
   }
 
   render () {
-    const { href, children, className, ...other } = this.props;
+    const { href, children, className, onInterceptClick, ...other } = this.props;
 
     return (
       <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index 71228ca6c..ac3e404df 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -17,7 +17,7 @@ export default class ScrollableList extends PureComponent {
 
   static propTypes = {
     scrollKey: PropTypes.string.isRequired,
-    onScrollToBottom: PropTypes.func,
+    onLoadMore: PropTypes.func.isRequired,
     onScrollToTop: PropTypes.func,
     onScroll: PropTypes.func,
     trackScroll: PropTypes.bool,
@@ -45,9 +45,11 @@ export default class ScrollableList extends PureComponent {
       const offset = scrollHeight - scrollTop - clientHeight;
       this._oldScrollPosition = scrollHeight - scrollTop;
 
-      if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
-        this.props.onScrollToBottom();
-      } else if (scrollTop < 100 && this.props.onScrollToTop) {
+      if (400 > offset && this.props.onLoadMore && !this.props.isLoading) {
+        this.props.onLoadMore();
+      }
+
+      if (scrollTop < 100 && this.props.onScrollToTop) {
         this.props.onScrollToTop();
       } else if (this.props.onScroll) {
         this.props.onScroll();
@@ -138,7 +140,7 @@ export default class ScrollableList extends PureComponent {
 
   handleLoadMore = (e) => {
     e.preventDefault();
-    this.props.onScrollToBottom();
+    this.props.onLoadMore();
   }
 
   _recentlyMoved () {
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index c52cd5f09..8102d1e06 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -7,6 +7,7 @@ import RelativeTimestamp from './relative_timestamp';
 import DisplayName from './display_name';
 import StatusContent from './status_content';
 import StatusActionBar from './status_action_bar';
+import AttachmentList from './attachment_list';
 import { FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { MediaGallery, Video } from '../features/ui/util/async-components';
@@ -138,7 +139,7 @@ export default class Status extends ImmutablePureComponent {
     let media = null;
     let statusAvatar, prepend;
 
-    const { hidden }     = this.props;
+    const { hidden, featured } = this.props;
     const { isExpanded } = this.state;
 
     let { status, account, ...other } = this.props;
@@ -156,7 +157,14 @@ export default class Status extends ImmutablePureComponent {
       );
     }
 
-    if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
+    if (featured) {
+      prepend = (
+        <div className='status__prepend'>
+          <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-thumb-tack status__prepend-icon' /></div>
+          <FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
+        </div>
+      );
+    } else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
       const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
 
       prepend = (
@@ -170,9 +178,14 @@ export default class Status extends ImmutablePureComponent {
       status  = status.get('reblog');
     }
 
-    if (status.get('media_attachments').size > 0 && !this.props.muted) {
-      if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
-
+    if (status.get('media_attachments').size > 0) {
+      if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
+        media = (
+          <AttachmentList
+            compact
+            media={status.get('media_attachments')}
+          />
+        );
       } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
         const video = status.getIn(['media_attachments', 0]);
 
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 3b8155632..701b5702c 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -24,7 +24,12 @@ export default class StatusContent extends React.PureComponent {
   };
 
   _updateStatusLinks () {
-    const node  = this.node;
+    const node = this.node;
+
+    if (!node) {
+      return;
+    }
+
     const links = node.querySelectorAll('a');
 
     for (var i = 0; i < links.length; ++i) {
@@ -115,6 +120,10 @@ export default class StatusContent extends React.PureComponent {
   render () {
     const { status } = this.props;
 
+    if (status.get('content').length === 0) {
+      return null;
+    }
+
     const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
 
     const content = { __html: status.get('contentHtml') };
diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js
index 5acaf714e..3bebf702c 100644
--- a/app/javascript/mastodon/components/status_list.js
+++ b/app/javascript/mastodon/components/status_list.js
@@ -11,7 +11,8 @@ export default class StatusList extends ImmutablePureComponent {
   static propTypes = {
     scrollKey: PropTypes.string.isRequired,
     statusIds: ImmutablePropTypes.list.isRequired,
-    onScrollToBottom: PropTypes.func,
+    featuredStatusIds: ImmutablePropTypes.list,
+    onLoadMore: PropTypes.func,
     onScrollToTop: PropTypes.func,
     onScroll: PropTypes.func,
     trackScroll: PropTypes.bool,
@@ -50,7 +51,7 @@ export default class StatusList extends ImmutablePureComponent {
   }
 
   render () {
-    const { statusIds, ...other }  = this.props;
+    const { statusIds, featuredStatusIds, ...other }  = this.props;
     const { isLoading, isPartial } = other;
 
     if (isPartial) {
@@ -68,8 +69,8 @@ export default class StatusList extends ImmutablePureComponent {
       );
     }
 
-    const scrollableContent = (isLoading || statusIds.size > 0) ? (
-      statusIds.map((statusId) => (
+    let scrollableContent = (isLoading || statusIds.size > 0) ? (
+      statusIds.map(statusId => (
         <StatusContainer
           key={statusId}
           id={statusId}
@@ -79,6 +80,18 @@ export default class StatusList extends ImmutablePureComponent {
       ))
     ) : null;
 
+    if (scrollableContent && featuredStatusIds) {
+      scrollableContent = featuredStatusIds.map(statusId => (
+        <StatusContainer
+          key={`f-${statusId}`}
+          id={statusId}
+          featured
+          onMoveUp={this.handleMoveUp}
+          onMoveDown={this.handleMoveDown}
+        />
+      )).concat(scrollableContent);
+    }
+
     return (
       <ScrollableList {...other} ref={this.setRef}>
         {scrollableContent}
diff --git a/app/javascript/mastodon/containers/dropdown_menu_container.js b/app/javascript/mastodon/containers/dropdown_menu_container.js
index 151f25390..7cbcdcd35 100644
--- a/app/javascript/mastodon/containers/dropdown_menu_container.js
+++ b/app/javascript/mastodon/containers/dropdown_menu_container.js
@@ -1,3 +1,4 @@
+import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu';
 import { openModal, closeModal } from '../actions/modal';
 import { connect } from 'react-redux';
 import DropdownMenu from '../components/dropdown_menu';
@@ -5,12 +6,22 @@ import { isUserTouching } from '../is_mobile';
 
 const mapStateToProps = state => ({
   isModalOpen: state.get('modal').modalType === 'ACTIONS',
+  dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
+  openDropdownId: state.getIn(['dropdown_menu', 'openId']),
 });
 
-const mapDispatchToProps = dispatch => ({
-  isUserTouching,
-  onModalOpen: props => dispatch(openModal('ACTIONS', props)),
-  onModalClose: () => dispatch(closeModal()),
+const mapDispatchToProps = (dispatch, { status, items }) => ({
+  onOpen(id, onItemClick, dropdownPlacement) {
+    dispatch(isUserTouching() ? openModal('ACTIONS', {
+      status,
+      actions: items,
+      onClick: onItemClick,
+    }) : openDropdownMenu(id, dropdownPlacement));
+  },
+  onClose(id) {
+    dispatch(closeModal());
+    dispatch(closeDropdownMenu(id));
+  },
 });
 
 export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index b8605d11f..bb7b3b632 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -13,6 +13,7 @@ const messages = defineMessages({
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
   follow: { id: 'account.follow', defaultMessage: 'Follow' },
   requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
+  unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
 });
 
 class Avatar extends ImmutablePureComponent {
@@ -69,6 +70,7 @@ export default class Header extends ImmutablePureComponent {
   static propTypes = {
     account: ImmutablePropTypes.map,
     onFollow: PropTypes.func.isRequired,
+    onBlock: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
   };
 
@@ -80,11 +82,20 @@ export default class Header extends ImmutablePureComponent {
     }
 
     let info        = '';
+    let mutingInfo  = '';
     let actionBtn   = '';
     let lockedIcon  = '';
 
     if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
       info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>;
+    } else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
+      info = <span className='account--follows-info'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>;
+    }
+
+    if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
+      mutingInfo = <span className='account--muting-info'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>;
+    } else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
+      mutingInfo = <span className='account--muting-info'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain hidden' /></span>;
     }
 
     if (me !== account.get('id')) {
@@ -100,6 +111,12 @@ export default class Header extends ImmutablePureComponent {
             <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />
           </div>
         );
+      } else if (account.getIn(['relationship', 'blocking'])) {
+        actionBtn = (
+          <div className='account--action-button'>
+            <IconButton size={26} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />
+          </div>
+        );
       }
     }
 
@@ -124,6 +141,7 @@ export default class Header extends ImmutablePureComponent {
           <div className='account__header__content' dangerouslySetInnerHTML={content} />
 
           {info}
+          {mutingInfo}
           {actionBtn}
         </div>
       </div>
diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js
index 59c805c38..f7a802dc7 100644
--- a/app/javascript/mastodon/features/account_gallery/components/media_item.js
+++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js
@@ -2,6 +2,7 @@ import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import Permalink from '../../../components/permalink';
+import { displaySensitiveMedia } from '../../../initial_state';
 
 export default class MediaItem extends ImmutablePureComponent {
 
@@ -9,8 +10,22 @@ export default class MediaItem extends ImmutablePureComponent {
     media: ImmutablePropTypes.map.isRequired,
   };
 
+  state = {
+    visible: !this.props.media.getIn(['status', 'sensitive']) || displaySensitiveMedia,
+  };
+
+  handleClick = () => {
+    if (!this.state.visible) {
+      this.setState({ visible: true });
+      return true;
+    }
+
+    return false;
+  }
+
   render () {
     const { media } = this.props;
+    const { visible } = this.state;
     const status = media.get('status');
     const focusX = media.getIn(['meta', 'focus', 'x']);
     const focusY = media.getIn(['meta', 'focus', 'y']);
@@ -18,21 +33,28 @@ export default class MediaItem extends ImmutablePureComponent {
     const y = ((focusY / -2) + .5) * 100;
     const style = {};
 
-    let content;
+    let label, icon;
 
     if (media.get('type') === 'gifv') {
-      content = <span className='media-gallery__gifv__label'>GIF</span>;
+      label = <span className='media-gallery__gifv__label'>GIF</span>;
     }
 
-    if (!status.get('sensitive')) {
+    if (visible) {
       style.backgroundImage    = `url(${media.get('preview_url')})`;
       style.backgroundPosition = `${x}% ${y}%`;
+    } else {
+      icon = (
+        <span className='account-gallery__item__icons'>
+          <i className='fa fa-eye-slash' />
+        </span>
+      );
     }
 
     return (
       <div className='account-gallery__item'>
-        <Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style}>
-          {content}
+        <Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style} onInterceptClick={this.handleClick}>
+          {icon}
+          {label}
         </Permalink>
       </div>
     );
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js
index 5cd4af1d3..9d594fb0c 100644
--- a/app/javascript/mastodon/features/account_timeline/components/header.js
+++ b/app/javascript/mastodon/features/account_timeline/components/header.js
@@ -21,6 +21,7 @@ export default class Header extends ImmutablePureComponent {
     onMute: PropTypes.func.isRequired,
     onBlockDomain: PropTypes.func.isRequired,
     onUnblockDomain: PropTypes.func.isRequired,
+    hideTabs: PropTypes.bool,
   };
 
   static contextTypes = {
@@ -68,7 +69,7 @@ export default class Header extends ImmutablePureComponent {
   }
 
   render () {
-    const { account } = this.props;
+    const { account, hideTabs } = this.props;
 
     if (account === null) {
       return <MissingIndicator />;
@@ -81,6 +82,7 @@ export default class Header extends ImmutablePureComponent {
         <InnerHeader
           account={account}
           onFollow={this.handleFollow}
+          onBlock={this.handleBlock}
         />
 
         <ActionBar
@@ -94,11 +96,13 @@ export default class Header extends ImmutablePureComponent {
           onUnblockDomain={this.handleUnblockDomain}
         />
 
-        <div className='account__section-headline'>
-          <NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
-          <NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
-          <NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
-        </div>
+        {!hideTabs && (
+          <div className='account__section-headline'>
+            <NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
+            <NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
+            <NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
+          </div>
+        )}
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index aed009ef0..f5f2475ea 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/app/javascript/mastodon/features/account_timeline/index.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import { fetchAccount } from '../../actions/accounts';
-import { refreshAccountTimeline, expandAccountTimeline } from '../../actions/timelines';
+import { refreshAccountTimeline, refreshAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
 import StatusList from '../../components/status_list';
 import LoadingIndicator from '../../components/loading_indicator';
 import Column from '../ui/components/column';
@@ -17,6 +17,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
 
   return {
     statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
+    featuredStatusIds: state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
     isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
     hasMore: !!state.getIn(['timelines', `account:${path}`, 'next']),
   };
@@ -29,31 +30,36 @@ export default class AccountTimeline extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     statusIds: ImmutablePropTypes.list,
+    featuredStatusIds: ImmutablePropTypes.list,
     isLoading: PropTypes.bool,
     hasMore: PropTypes.bool,
     withReplies: PropTypes.bool,
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchAccount(this.props.params.accountId));
-    this.props.dispatch(refreshAccountTimeline(this.props.params.accountId, this.props.withReplies));
+    const { params: { accountId }, withReplies } = this.props;
+
+    this.props.dispatch(fetchAccount(accountId));
+    this.props.dispatch(refreshAccountFeaturedTimeline(accountId));
+    this.props.dispatch(refreshAccountTimeline(accountId, withReplies));
   }
 
   componentWillReceiveProps (nextProps) {
     if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
       this.props.dispatch(fetchAccount(nextProps.params.accountId));
+      this.props.dispatch(refreshAccountFeaturedTimeline(nextProps.params.accountId));
       this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId, nextProps.params.withReplies));
     }
   }
 
-  handleScrollToBottom = () => {
+  handleLoadMore = () => {
     if (!this.props.isLoading && this.props.hasMore) {
       this.props.dispatch(expandAccountTimeline(this.props.params.accountId, this.props.withReplies));
     }
   }
 
   render () {
-    const { statusIds, isLoading, hasMore } = this.props;
+    const { statusIds, featuredStatusIds, isLoading, hasMore } = this.props;
 
     if (!statusIds && isLoading) {
       return (
@@ -71,9 +77,10 @@ export default class AccountTimeline extends ImmutablePureComponent {
           prepend={<HeaderContainer accountId={this.props.params.accountId} />}
           scrollKey='account_timeline'
           statusIds={statusIds}
+          featuredStatusIds={featuredStatusIds}
           isLoading={isLoading}
           hasMore={hasMore}
-          onScrollToBottom={this.handleScrollToBottom}
+          onLoadMore={this.handleLoadMore}
         />
       </Column>
     );
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index a876c5197..663ccfb8e 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -50,6 +50,7 @@ export default class ComposeForm extends ImmutablePureComponent {
     onPaste: PropTypes.func.isRequired,
     onPickEmoji: PropTypes.func.isRequired,
     showSearch: PropTypes.bool,
+    anyMedia: PropTypes.bool,
   };
 
   static defaultProps = {
@@ -142,10 +143,10 @@ export default class ComposeForm extends ImmutablePureComponent {
   }
 
   render () {
-    const { intl, onPaste, showSearch } = this.props;
+    const { intl, onPaste, showSearch, anyMedia } = this.props;
     const disabled = this.props.is_submitting;
     const text     = [this.props.spoiler_text, countableText(this.props.text)].join('');
-
+    const disabledButton = disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
     let publishText = '';
 
     if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
@@ -203,7 +204,7 @@ export default class ComposeForm extends ImmutablePureComponent {
         </div>
 
         <div className='compose-form__publish'>
-          <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
+          <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabledButton} block /></div>
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js
index 5f5509dbe..ede23d361 100644
--- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js
+++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js
@@ -23,6 +23,7 @@ const mapStateToProps = state => ({
   is_submitting: state.getIn(['compose', 'is_submitting']),
   is_uploading: state.getIn(['compose', 'is_uploading']),
   showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
+  anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
 });
 
 const mapDispatchToProps = (dispatch) => ({
diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js
index 67b107bc8..6f1c863b4 100644
--- a/app/javascript/mastodon/features/favourited_statuses/index.js
+++ b/app/javascript/mastodon/features/favourited_statuses/index.js
@@ -62,7 +62,7 @@ export default class Favourites extends ImmutablePureComponent {
     this.column = c;
   }
 
-  handleScrollToBottom = debounce(() => {
+  handleLoadMore = debounce(() => {
     this.props.dispatch(expandFavouritedStatuses());
   }, 300, { leading: true })
 
@@ -89,7 +89,7 @@ export default class Favourites extends ImmutablePureComponent {
           scrollKey={`favourited_statuses-${columnId}`}
           hasMore={hasMore}
           isLoading={isLoading}
-          onScrollToBottom={this.handleScrollToBottom}
+          onLoadMore={this.handleLoadMore}
         />
       </Column>
     );
diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js
index f64ed7948..919a89332 100644
--- a/app/javascript/mastodon/features/followers/index.js
+++ b/app/javascript/mastodon/features/followers/index.js
@@ -80,7 +80,7 @@ export default class Followers extends ImmutablePureComponent {
         <ScrollContainer scrollKey='followers'>
           <div className='scrollable' onScroll={this.handleScroll}>
             <div className='followers'>
-              <HeaderContainer accountId={this.props.params.accountId} />
+              <HeaderContainer accountId={this.props.params.accountId} hideTabs />
               {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
               {loadMore}
             </div>
diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js
index a0c0fac05..5719259d1 100644
--- a/app/javascript/mastodon/features/following/index.js
+++ b/app/javascript/mastodon/features/following/index.js
@@ -80,7 +80,7 @@ export default class Following extends ImmutablePureComponent {
         <ScrollContainer scrollKey='following'>
           <div className='scrollable' onScroll={this.handleScroll}>
             <div className='following'>
-              <HeaderContainer accountId={this.props.params.accountId} />
+              <HeaderContainer accountId={this.props.params.accountId} hideTabs />
               {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
               {loadMore}
             </div>
diff --git a/app/javascript/mastodon/features/notifications/components/clear_column_button.js b/app/javascript/mastodon/features/notifications/components/clear_column_button.js
index 22a10753f..e0bf4c82d 100644
--- a/app/javascript/mastodon/features/notifications/components/clear_column_button.js
+++ b/app/javascript/mastodon/features/notifications/components/clear_column_button.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { FormattedMessage } from 'react-intl';
 
-export default class ClearColumnButton extends React.Component {
+export default class ClearColumnButton extends React.PureComponent {
 
   static propTypes = {
     onClick: PropTypes.func.isRequired,
diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js
index 35b430bfb..cb9d025ea 100644
--- a/app/javascript/mastodon/features/notifications/index.js
+++ b/app/javascript/mastodon/features/notifications/index.js
@@ -50,8 +50,14 @@ export default class Notifications extends React.PureComponent {
     trackScroll: true,
   };
 
-  handleScrollToBottom = debounce(() => {
+  componentWillUnmount () {
+    this.handleLoadMore.cancel();
+    this.handleScrollToTop.cancel();
+    this.handleScroll.cancel();
     this.props.dispatch(scrollTopNotifications(false));
+  }
+
+  handleLoadMore = debounce(() => {
     this.props.dispatch(expandNotifications());
   }, 300, { leading: true });
 
@@ -136,7 +142,7 @@ export default class Notifications extends React.PureComponent {
         isLoading={isLoading}
         hasMore={hasMore}
         emptyMessage={emptyMessage}
-        onScrollToBottom={this.handleScrollToBottom}
+        onLoadMore={this.handleLoadMore}
         onScrollToTop={this.handleScrollToTop}
         onScroll={this.handleScroll}
         shouldUpdateScroll={shouldUpdateScroll}
diff --git a/app/javascript/mastodon/features/report/components/status_check_box.js b/app/javascript/mastodon/features/report/components/status_check_box.js
index cc9232201..9ff75a082 100644
--- a/app/javascript/mastodon/features/report/components/status_check_box.js
+++ b/app/javascript/mastodon/features/report/components/status_check_box.js
@@ -2,6 +2,10 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import Toggle from 'react-toggle';
+import noop from 'lodash/noop';
+import StatusContent from '../../../components/status_content';
+import { MediaGallery, Video } from '../../ui/util/async-components';
+import Bundle from '../../ui/components/bundle';
 
 export default class StatusCheckBox extends React.PureComponent {
 
@@ -14,18 +18,48 @@ export default class StatusCheckBox extends React.PureComponent {
 
   render () {
     const { status, checked, onToggle, disabled } = this.props;
-    const content = { __html: status.get('contentHtml') };
+    let media = null;
 
     if (status.get('reblog')) {
       return null;
     }
 
+    if (status.get('media_attachments').size > 0) {
+      if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
+
+      } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+        const video = status.getIn(['media_attachments', 0]);
+
+        media = (
+          <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
+            {Component => (
+              <Component
+                preview={video.get('preview_url')}
+                src={video.get('url')}
+                width={239}
+                height={110}
+                inline
+                sensitive={status.get('sensitive')}
+                onOpenVideo={noop}
+              />
+            )}
+          </Bundle>
+        );
+      } else {
+        media = (
+          <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
+            {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={noop} />}
+          </Bundle>
+        );
+      }
+    }
+
     return (
       <div className='status-check-box'>
-        <div
-          className='status__content'
-          dangerouslySetInnerHTML={content}
-        />
+        <div className='status-check-box__status'>
+          <StatusContent status={status} />
+          {media}
+        </div>
 
         <div className='status-check-box-toggle'>
           <Toggle checked={checked} onChange={onToggle} disabled={disabled} />
diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.js
index 06a6c9cdd..e7d935251 100644
--- a/app/javascript/mastodon/features/ui/components/bundle.js
+++ b/app/javascript/mastodon/features/ui/components/bundle.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 const emptyComponent = () => null;
 const noop = () => { };
 
-class Bundle extends React.Component {
+class Bundle extends React.PureComponent {
 
   static propTypes = {
     fetchComponent: PropTypes.func.isRequired,
diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error.js b/app/javascript/mastodon/features/ui/components/bundle_column_error.js
index cd124746a..f39ebd900 100644
--- a/app/javascript/mastodon/features/ui/components/bundle_column_error.js
+++ b/app/javascript/mastodon/features/ui/components/bundle_column_error.js
@@ -13,7 +13,7 @@ const messages = defineMessages({
   retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' },
 });
 
-class BundleColumnError extends React.Component {
+class BundleColumnError extends React.PureComponent {
 
   static propTypes = {
     onRetry: PropTypes.func.isRequired,
diff --git a/app/javascript/mastodon/features/ui/components/bundle_modal_error.js b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js
index 928bfe1f7..f9365b95b 100644
--- a/app/javascript/mastodon/features/ui/components/bundle_modal_error.js
+++ b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js
@@ -10,7 +10,7 @@ const messages = defineMessages({
   close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
 });
 
-class BundleModalError extends React.Component {
+class BundleModalError extends React.PureComponent {
 
   static propTypes = {
     onRetry: PropTypes.func.isRequired,
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
index ee5c791d4..1038e1864 100644
--- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js
+++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
@@ -103,8 +103,8 @@ export default class FocalPointModal extends ImmutablePureComponent {
     const height = media.getIn(['meta', 'original', 'height']) || null;
 
     return (
-      <div className='modal-root__modal media-modal'>
-        <div className={classNames('media-modal__content focal-point', { dragging })} ref={this.setRef}>
+      <div className='modal-root__modal video-modal'>
+        <div className={classNames('focal-point', { dragging })} ref={this.setRef}>
           <ImageLoader
             previewSrc={media.get('preview_url')}
             src={media.get('url')}
diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js
index e3e7197c5..c7360a726 100644
--- a/app/javascript/mastodon/features/ui/components/image_loader.js
+++ b/app/javascript/mastodon/features/ui/components/image_loader.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
+import ZoomableImage from './zoomable_image';
 
 export default class ImageLoader extends React.PureComponent {
 
@@ -10,6 +11,7 @@ export default class ImageLoader extends React.PureComponent {
     previewSrc: PropTypes.string,
     width: PropTypes.number,
     height: PropTypes.number,
+    onClick: PropTypes.func,
   }
 
   static defaultProps = {
@@ -24,6 +26,7 @@ export default class ImageLoader extends React.PureComponent {
   }
 
   removers = [];
+  canvas = null;
 
   get canvasContext() {
     if (!this.canvas) {
@@ -43,6 +46,10 @@ export default class ImageLoader extends React.PureComponent {
     }
   }
 
+  componentWillUnmount () {
+    this.removeEventListeners();
+  }
+
   loadImage (props) {
     this.removeEventListeners();
     this.setState({ loading: true, error: false });
@@ -118,7 +125,7 @@ export default class ImageLoader extends React.PureComponent {
   }
 
   render () {
-    const { alt, src, width, height } = this.props;
+    const { alt, src, width, height, onClick } = this.props;
     const { loading } = this.state;
 
     const className = classNames('image-loader', {
@@ -128,22 +135,19 @@ export default class ImageLoader extends React.PureComponent {
 
     return (
       <div className={className}>
-        <canvas
-          className='image-loader__preview-canvas'
-          width={width}
-          height={height}
-          ref={this.setCanvasRef}
-          style={{ opacity: loading ? 1 : 0 }}
-        />
-
-        {!loading && (
-          <img
-            alt={alt}
-            className='image-loader__img'
-            src={src}
+        {loading ? (
+          <canvas
+            className='image-loader__preview-canvas'
+            ref={this.setCanvasRef}
             width={width}
             height={height}
           />
+        ) : (
+          <ZoomableImage
+            alt={alt}
+            src={src}
+            onClick={onClick}
+          />
         )}
       </div>
     );
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js
index 02591a51f..72ef32256 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.js
@@ -3,6 +3,7 @@ import ReactSwipeableViews from 'react-swipeable-views';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import ExtendedVideoPlayer from '../../../components/extended_video_player';
+import classNames from 'classnames';
 import { defineMessages, injectIntl } from 'react-intl';
 import IconButton from '../../../components/icon_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -26,6 +27,7 @@ export default class MediaModal extends ImmutablePureComponent {
 
   state = {
     index: null,
+    navigationHidden: false,
   };
 
   handleSwipe = (index) => {
@@ -68,14 +70,21 @@ export default class MediaModal extends ImmutablePureComponent {
     return this.state.index !== null ? this.state.index : this.props.index;
   }
 
+  toggleNavigation = () => {
+    this.setState(prevState => ({
+      navigationHidden: !prevState.navigationHidden,
+    }));
+  };
+
   render () {
     const { media, intl, onClose } = this.props;
+    const { navigationHidden } = this.state;
 
     const index = this.getIndex();
     let pagination = [];
 
-    const leftNav  = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>;
-    const rightNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav  modal-container__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>;
+    const leftNav  = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>;
+    const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav  media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>;
 
     if (media.size > 1) {
       pagination = media.map((item, i) => {
@@ -92,9 +101,30 @@ 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('url')} />;
+        return (
+          <ImageLoader
+            previewSrc={image.get('preview_url')}
+            src={image.get('url')}
+            width={width}
+            height={height}
+            alt={image.get('description')}
+            key={image.get('url')}
+            onClick={this.toggleNavigation}
+          />
+        );
       } 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')} />;
+        return (
+          <ExtendedVideoPlayer
+            src={image.get('url')}
+            muted
+            controls={false}
+            width={width}
+            height={height}
+            key={image.get('preview_url')}
+            alt={image.get('description')}
+            onClick={this.toggleNavigation}
+          />
+        );
       }
 
       return null;
@@ -104,21 +134,43 @@ export default class MediaModal extends ImmutablePureComponent {
       alignItems: 'center', // center vertically
     };
 
+    const navigationClassName = classNames('media-modal__navigation', {
+      'media-modal__navigation--hidden': navigationHidden,
+    });
+
     return (
       <div className='modal-root__modal media-modal'>
-        {leftNav}
-
-        <div className='media-modal__content'>
-          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
-          <ReactSwipeableViews containerStyle={containerStyle} onChangeIndex={this.handleSwipe} index={index}>
-            {content}
-          </ReactSwipeableViews>
+        <div
+          className='media-modal__closer'
+          role='presentation'
+          onClick={onClose}
+        >
+          <div className='media-modal__content'>
+            <ReactSwipeableViews
+              style={{
+                // you can't use 100vh, because the viewport height is taller
+                // than the visible part of the document in some mobile
+                // browsers when it's address bar is visible.
+                // https://developers.google.com/web/updates/2016/12/url-bar-resizing
+                height: `${document.body.clientHeight}px`,
+              }}
+              containerStyle={containerStyle}
+              onChangeIndex={this.handleSwipe}
+              onSwitching={this.handleSwitching}
+              index={index}
+            >
+              {content}
+            </ReactSwipeableViews>
+          </div>
+        </div>
+        <div className={navigationClassName}>
+          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} />
+          {leftNav}
+          {rightNav}
+          <ul className='media-modal__pagination'>
+            {pagination}
+          </ul>
         </div>
-        <ul className='media-modal__pagination'>
-          {pagination}
-        </ul>
-
-        {rightNav}
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js
index 77fe5f5e2..dba3be98b 100644
--- a/app/javascript/mastodon/features/ui/components/tabs_bar.js
+++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { NavLink } from 'react-router-dom';
+import { NavLink, withRouter } from 'react-router-dom';
 import { FormattedMessage, injectIntl } from 'react-intl';
 import { debounce } from 'lodash';
 import { isUserTouching } from '../../../is_mobile';
@@ -24,14 +24,12 @@ export function getLink (index) {
 }
 
 @injectIntl
-export default class TabsBar extends React.Component {
-
-  static contextTypes = {
-    router: PropTypes.object.isRequired,
-  }
+@withRouter
+export default class TabsBar extends React.PureComponent {
 
   static propTypes = {
     intl: PropTypes.object.isRequired,
+    history: PropTypes.object.isRequired,
   }
 
   setRef = ref => {
@@ -59,7 +57,7 @@ export default class TabsBar extends React.Component {
 
           const listener = debounce(() => {
             nextTab.removeEventListener('transitionend', listener);
-            this.context.router.history.push(to);
+            this.props.history.push(to);
           }, 50);
 
           nextTab.addEventListener('transitionend', listener);
diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js
index 6a883759f..9ed4a43ad 100644
--- a/app/javascript/mastodon/features/ui/components/video_modal.js
+++ b/app/javascript/mastodon/features/ui/components/video_modal.js
@@ -16,7 +16,7 @@ export default class VideoModal extends ImmutablePureComponent {
     const { media, time, onClose } = this.props;
 
     return (
-      <div className='modal-root__modal media-modal'>
+      <div className='modal-root__modal video-modal'>
         <div>
           <Video
             preview={media.get('preview_url')}
diff --git a/app/javascript/mastodon/features/ui/components/zoomable_image.js b/app/javascript/mastodon/features/ui/components/zoomable_image.js
new file mode 100644
index 000000000..0a0a4d41a
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/zoomable_image.js
@@ -0,0 +1,151 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const MIN_SCALE = 1;
+const MAX_SCALE = 4;
+
+const getMidpoint = (p1, p2) => ({
+  x: (p1.clientX + p2.clientX) / 2,
+  y: (p1.clientY + p2.clientY) / 2,
+});
+
+const getDistance = (p1, p2) =>
+  Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2));
+
+const clamp = (min, max, value) => Math.min(max, Math.max(min, value));
+
+export default class ZoomableImage extends React.PureComponent {
+
+  static propTypes = {
+    alt: PropTypes.string,
+    src: PropTypes.string.isRequired,
+    width: PropTypes.number,
+    height: PropTypes.number,
+    onClick: PropTypes.func,
+  }
+
+  static defaultProps = {
+    alt: '',
+    width: null,
+    height: null,
+  };
+
+  state = {
+    scale: MIN_SCALE,
+  }
+
+  removers = [];
+  container = null;
+  image = null;
+  lastTouchEndTime = 0;
+  lastDistance = 0;
+
+  componentDidMount () {
+    let handler = this.handleTouchStart;
+    this.container.addEventListener('touchstart', handler);
+    this.removers.push(() => this.container.removeEventListener('touchstart', handler));
+    handler = this.handleTouchMove;
+    // on Chrome 56+, touch event listeners will default to passive
+    // https://www.chromestatus.com/features/5093566007214080
+    this.container.addEventListener('touchmove', handler, { passive: false });
+    this.removers.push(() => this.container.removeEventListener('touchend', handler));
+  }
+
+  componentWillUnmount () {
+    this.removeEventListeners();
+  }
+
+  removeEventListeners () {
+    this.removers.forEach(listeners => listeners());
+    this.removers = [];
+  }
+
+  handleTouchStart = e => {
+    if (e.touches.length !== 2) return;
+
+    this.lastDistance = getDistance(...e.touches);
+  }
+
+  handleTouchMove = e => {
+    const { scrollTop, scrollHeight, clientHeight } = this.container;
+    if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) {
+      // prevent propagating event to MediaModal
+      e.stopPropagation();
+      return;
+    }
+    if (e.touches.length !== 2) return;
+
+    e.preventDefault();
+    e.stopPropagation();
+
+    const distance = getDistance(...e.touches);
+    const midpoint = getMidpoint(...e.touches);
+    const scale = clamp(MIN_SCALE, MAX_SCALE, this.state.scale * distance / this.lastDistance);
+
+    this.zoom(scale, midpoint);
+
+    this.lastMidpoint = midpoint;
+    this.lastDistance = distance;
+  }
+
+  zoom(nextScale, midpoint) {
+    const { scale } = this.state;
+    const { scrollLeft, scrollTop } = this.container;
+
+    // math memo:
+    // x = (scrollLeft + midpoint.x) / scrollWidth
+    // x' = (nextScrollLeft + midpoint.x) / nextScrollWidth
+    // scrollWidth = clientWidth * scale
+    // scrollWidth' = clientWidth * nextScale
+    // Solve x = x' for nextScrollLeft
+    const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x;
+    const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y;
+
+    this.setState({ scale: nextScale }, () => {
+      this.container.scrollLeft = nextScrollLeft;
+      this.container.scrollTop = nextScrollTop;
+    });
+  }
+
+  handleClick = e => {
+    // don't propagate event to MediaModal
+    e.stopPropagation();
+    const handler = this.props.onClick;
+    if (handler) handler();
+  }
+
+  setContainerRef = c => {
+    this.container = c;
+  }
+
+  setImageRef = c => {
+    this.image = c;
+  }
+
+  render () {
+    const { alt, src } = this.props;
+    const { scale } = this.state;
+    const overflow = scale === 1 ? 'hidden' : 'scroll';
+
+    return (
+      <div
+        className='zoomable-image'
+        ref={this.setContainerRef}
+        style={{ overflow }}
+      >
+        <img
+          role='presentation'
+          ref={this.setImageRef}
+          alt={alt}
+          src={src}
+          style={{
+            transform: `scale(${scale})`,
+            transformOrigin: '0 0',
+          }}
+          onClick={this.handleClick}
+        />
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js
index 59b53d823..fc2867cf0 100644
--- a/app/javascript/mastodon/features/ui/containers/status_list_container.js
+++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js
@@ -56,10 +56,7 @@ const makeMapStateToProps = () => {
 
 const mapDispatchToProps = (dispatch, { timelineId, loadMore }) => ({
 
-  onScrollToBottom: debounce(() => {
-    dispatch(scrollTopTimeline(timelineId, false));
-    loadMore();
-  }, 300, { leading: true }),
+  onLoadMore: debounce(loadMore, 300, { leading: true }),
 
   onScrollToTop: debounce(() => {
     dispatch(scrollTopTimeline(timelineId, true));
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index ef909136f..6cf00222a 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -1,3 +1,4 @@
+import classNames from 'classnames';
 import React from 'react';
 import NotificationsContainer from './containers/notifications_container';
 import PropTypes from 'prop-types';
@@ -55,6 +56,7 @@ const messages = defineMessages({
 const mapStateToProps = state => ({
   isComposing: state.getIn(['compose', 'is_composing']),
   hasComposingText: state.getIn(['compose', 'text']) !== '',
+  dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
 });
 
 const keyMap = {
@@ -84,10 +86,93 @@ const keyMap = {
   goToMuted: 'g m',
 };
 
+class SwitchingColumnsArea extends React.PureComponent {
+
+  static propTypes = {
+    children: PropTypes.node,
+    location: PropTypes.object,
+    onLayoutChange: PropTypes.func.isRequired,
+  };
+
+  state = {
+    mobile: isMobile(window.innerWidth),
+  };
+
+  componentWillMount () {
+    window.addEventListener('resize', this.handleResize, { passive: true });
+  }
+
+  componentDidUpdate (prevProps) {
+    if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
+      this.node.handleChildrenContentChange();
+    }
+  }
+
+  componentWillUnmount () {
+    window.removeEventListener('resize', this.handleResize);
+  }
+
+  handleResize = debounce(() => {
+    // The cached heights are no longer accurate, invalidate
+    this.props.onLayoutChange();
+
+    this.setState({ mobile: isMobile(window.innerWidth) });
+  }, 500, {
+    trailing: true,
+  });
+
+  setRef = c => {
+    this.node = c.getWrappedInstance().getWrappedInstance();
+  }
+
+  render () {
+    const { children } = this.props;
+    const { mobile } = this.state;
+
+    return (
+      <ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}>
+        <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} />
+          <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
+
+          <WrappedRoute path='/statuses/new' component={Compose} content={children} />
+          <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
+          <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
+          <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
+
+          <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
+          <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
+          <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
+          <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
+          <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />
+
+          <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
+          <WrappedRoute path='/blocks' component={Blocks} content={children} />
+          <WrappedRoute path='/mutes' component={Mutes} content={children} />
+          <WrappedRoute path='/lists' component={Lists} content={children} />
+
+          <WrappedRoute component={GenericNotFound} content={children} />
+        </WrappedSwitch>
+      </ColumnsAreaContainer>
+    );
+  }
+
+}
+
 @connect(mapStateToProps)
 @injectIntl
 @withRouter
-export default class UI extends React.Component {
+export default class UI extends React.PureComponent {
 
   static contextTypes = {
     router: PropTypes.object.isRequired,
@@ -100,10 +185,10 @@ export default class UI extends React.Component {
     hasComposingText: PropTypes.bool,
     location: PropTypes.object,
     intl: PropTypes.object.isRequired,
+    dropdownMenuIsOpen: PropTypes.bool,
   };
 
   state = {
-    width: window.innerWidth,
     draggingOver: false,
   };
 
@@ -118,14 +203,10 @@ export default class UI extends React.Component {
     }
   }
 
-  handleResize = debounce(() => {
+  handleLayoutChange = () => {
     // The cached heights are no longer accurate, invalidate
     this.props.dispatch(clearHeight());
-
-    this.setState({ width: window.innerWidth });
-  }, 500, {
-    trailing: true,
-  });
+  }
 
   handleDragEnter = (e) => {
     e.preventDefault();
@@ -193,7 +274,6 @@ export default class UI extends React.Component {
 
   componentWillMount () {
     window.addEventListener('beforeunload', this.handleBeforeUnload, false);
-    window.addEventListener('resize', this.handleResize, { passive: true });
     document.addEventListener('dragenter', this.handleDragEnter, false);
     document.addEventListener('dragover', this.handleDragOver, false);
     document.addEventListener('drop', this.handleDrop, false);
@@ -214,28 +294,8 @@ export default class UI extends React.Component {
     };
   }
 
-  shouldComponentUpdate (nextProps) {
-    if (nextProps.isComposing !== this.props.isComposing) {
-      // Avoid expensive update just to toggle a class
-      this.node.classList.toggle('is-composing', nextProps.isComposing);
-
-      return false;
-    }
-
-    // Why isn't this working?!?
-    // return super.shouldComponentUpdate(nextProps, nextState);
-    return true;
-  }
-
-  componentDidUpdate (prevProps) {
-    if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
-      this.columnsAreaNode.handleChildrenContentChange();
-    }
-  }
-
   componentWillUnmount () {
     window.removeEventListener('beforeunload', this.handleBeforeUnload);
-    window.removeEventListener('resize', this.handleResize);
     document.removeEventListener('dragenter', this.handleDragEnter);
     document.removeEventListener('dragover', this.handleDragOver);
     document.removeEventListener('drop', this.handleDrop);
@@ -247,10 +307,6 @@ export default class UI extends React.Component {
     this.node = c;
   }
 
-  setColumnsAreaRef = c => {
-    this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance();
-  }
-
   handleHotkeyNew = e => {
     e.preventDefault();
 
@@ -350,8 +406,8 @@ export default class UI extends React.Component {
   }
 
   render () {
-    const { width, draggingOver } = this.state;
-    const { children } = this.props;
+    const { draggingOver } = this.state;
+    const { children, isComposing, location, dropdownMenuIsOpen } = this.props;
 
     const handlers = {
       help: this.handleHotkeyToggleHelp,
@@ -374,43 +430,12 @@ export default class UI extends React.Component {
 
     return (
       <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef}>
-        <div className='ui' ref={this.setRef}>
+        <div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
           <TabsBar />
 
-          <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width)}>
-            <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} />
-              <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
-
-              <WrappedRoute path='/statuses/new' component={Compose} content={children} />
-              <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
-              <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
-              <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
-
-              <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
-              <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
-              <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
-              <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
-              <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />
-
-              <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
-              <WrappedRoute path='/blocks' component={Blocks} content={children} />
-              <WrappedRoute path='/mutes' component={Mutes} content={children} />
-              <WrappedRoute path='/lists' component={Lists} content={children} />
-
-              <WrappedRoute component={GenericNotFound} content={children} />
-            </WrappedSwitch>
-          </ColumnsAreaContainer>
+          <SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}>
+            {children}
+          </SwitchingColumnsArea>
 
           <NotificationsContainer />
           <LoadingBarContainer className='loading-bar' />
diff --git a/app/javascript/mastodon/load_polyfills.js b/app/javascript/mastodon/load_polyfills.js
index 8927b7358..815e1905b 100644
--- a/app/javascript/mastodon/load_polyfills.js
+++ b/app/javascript/mastodon/load_polyfills.js
@@ -14,6 +14,7 @@ function loadPolyfills() {
   const needsBasePolyfills = !(
     window.Intl &&
     Object.assign &&
+    Object.values &&
     Number.isNaN &&
     window.Symbol &&
     Array.prototype.includes
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index b326bc3b1..33e223b2a 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -1,7 +1,9 @@
 {
   "account.block": "حظر @{name}",
   "account.block_domain": "إخفاء كل شيئ قادم من إسم النطاق {domain}",
+  "account.blocked": "محظور",
   "account.disclaimer_full": "قد لا تعكس المعلومات أدناه الملف الشخصي الكامل للمستخدم.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "تعديل الملف الشخصي",
   "account.follow": "تابِع",
   "account.followers": "المتابعون",
@@ -13,8 +15,9 @@
   "account.moved_to": "{name} إنتقل إلى :",
   "account.mute": "أكتم @{name}",
   "account.mute_notifications": "كتم إخطارات @{name}",
+  "account.muted": "مكتوم",
   "account.posts": "التبويقات",
-  "account.posts_with_replies": "Toots with replies",
+  "account.posts_with_replies": "تبويقات تحتوي على رُدود",
   "account.report": "أبلغ عن @{name}",
   "account.requested": "في انتظار الموافقة",
   "account.share": "مشاركة @{name}'s profile",
@@ -208,21 +211,22 @@
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "إلغاء",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
+  "report.forward": "التحويل إلى {target}",
+  "report.forward_hint": "هذا الحساب ينتمي إلى خادوم آخَر. هل تودّ إرسال نسخة مجهولة مِن التقرير إلى هنالك أيضًا ؟",
   "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
   "report.placeholder": "تعليقات إضافية",
   "report.submit": "إرسال",
   "report.target": "إبلاغ",
   "search.placeholder": "ابحث",
   "search_popout.search_format": "نمط البحث المتقدم",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "وسم",
   "search_popout.tips.status": "حالة",
   "search_popout.tips.text": "جملة قصيرة تُمكّنُك من عرض أسماء و حسابات و كلمات رمزية",
   "search_popout.tips.user": "مستخدِم",
-  "search_results.accounts": "People",
-  "search_results.hashtags": "Hashtags",
-  "search_results.statuses": "Toots",
+  "search_results.accounts": "أشخاص",
+  "search_results.hashtags": "الوُسوم",
+  "search_results.statuses": "التبويقات",
   "search_results.total": "{count, number} {count, plural, one {result} و {results}}",
   "standalone.public_title": "نظرة على ...",
   "status.block": "Block @{name}",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "كتم المحادثة",
   "status.open": "وسع هذه المشاركة",
   "status.pin": "تدبيس على الملف الشخصي",
+  "status.pinned": "تبويق مثبَّت",
   "status.reblog": "رَقِّي",
   "status.reblogged_by": "{name} رقى",
   "status.reply": "ردّ",
@@ -250,7 +255,6 @@
   "status.show_more": "أظهر المزيد",
   "status.unmute_conversation": "فك الكتم عن المحادثة",
   "status.unpin": "فك التدبيس من الملف الشخصي",
-  "tabs_bar.compose": "تحرير",
   "tabs_bar.federated_timeline": "الموحَّد",
   "tabs_bar.home": "الرئيسية",
   "tabs_bar.local_timeline": "المحلي",
@@ -259,7 +263,7 @@
   "upload_area.title": "إسحب ثم أفلت للرفع",
   "upload_button.label": "إضافة وسائط",
   "upload_form.description": "وصف للمعاقين بصريا",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "قص",
   "upload_form.undo": "إلغاء",
   "upload_progress.label": "يرفع...",
   "video.close": "إغلاق الفيديو",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 90e394a19..a84e6e9d1 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Блокирай",
   "account.block_domain": "Hide everything from {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Редактирай профила си",
   "account.follow": "Последвай",
   "account.followers": "Последователи",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} has moved to:",
   "account.mute": "Mute @{name}",
   "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
   "account.posts": "Публикации",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Report @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Reporting",
   "search.placeholder": "Търсене",
   "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
   "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Споделяне",
   "status.reblogged_by": "{name} сподели",
   "status.reply": "Отговор",
@@ -250,7 +255,6 @@
   "status.show_more": "Show more",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
-  "tabs_bar.compose": "Съставяне",
   "tabs_bar.federated_timeline": "Federated",
   "tabs_bar.home": "Начало",
   "tabs_bar.local_timeline": "Local",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 95b75cfc9..bac807dbb 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Bloca @{name}",
   "account.block_domain": "Amaga-ho tot de {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "La informació següent pot reflectir incompleta el perfil de l'usuari.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Edita el perfil",
   "account.follow": "Segueix",
   "account.followers": "Seguidors",
@@ -13,8 +15,9 @@
   "account.moved_to": "{name} s'ha mogut a:",
   "account.mute": "Silencia @{name}",
   "account.mute_notifications": "Notificacions desactivades de @{name}",
+  "account.muted": "Muted",
   "account.posts": "Toots",
-  "account.posts_with_replies": "Toots with replies",
+  "account.posts_with_replies": "Toots amb respostes",
   "account.report": "Informe @{name}",
   "account.requested": "Esperant aprovació. Clic per a cancel·lar la petició de seguiment",
   "account.share": "Comparteix el perfil de @{name}",
@@ -208,20 +211,21 @@
   "relative_time.minutes": "fa {number} minuts",
   "relative_time.seconds": "fa {number} segons",
   "reply_indicator.cancel": "Cancel·lar",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.forward": "Reenvia a {target}",
+  "report.forward_hint": "Aquest compte és d'un altre servidor. Enviar-hi també una copia anònima del informe?",
+  "report.hint": "El informe s'enviarà als moderadors de la teva instància. Pots explicar perquè vols informar d'aquest compte aquí:",
   "report.placeholder": "Comentaris addicionals",
   "report.submit": "Enviar",
   "report.target": "Informes",
   "search.placeholder": "Cercar",
   "search_popout.search_format": "Format de cerca avançada",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "etiqueta",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i els hashtags",
   "search_popout.tips.user": "usuari",
-  "search_results.accounts": "People",
-  "search_results.hashtags": "Hashtags",
+  "search_results.accounts": "Gent",
+  "search_results.hashtags": "Etiquetes",
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, un {result} altres {results}}",
   "standalone.public_title": "Una mirada a l'interior ...",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Silenciar conversació",
   "status.open": "Ampliar aquest estat",
   "status.pin": "Fixat en el perfil",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Impuls",
   "status.reblogged_by": "{name} ha retootejat",
   "status.reply": "Respondre",
@@ -250,7 +255,6 @@
   "status.show_more": "Mostra més",
   "status.unmute_conversation": "Activar conversació",
   "status.unpin": "Deslliga del perfil",
-  "tabs_bar.compose": "Compondre",
   "tabs_bar.federated_timeline": "Federada",
   "tabs_bar.home": "Inici",
   "tabs_bar.local_timeline": "Local",
@@ -259,7 +263,7 @@
   "upload_area.title": "Arrossega i deixa anar per carregar",
   "upload_button.label": "Afegir multimèdia",
   "upload_form.description": "Descriure els problemes visuals",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "Retallar",
   "upload_form.undo": "Desfer",
   "upload_progress.label": "Pujant...",
   "video.close": "Tancar el vídeo",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 7f29f83ce..6e0b4456c 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -1,7 +1,9 @@
 {
   "account.block": "@{name} blocken",
   "account.block_domain": "Alles von {domain} verstecken",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Das Profil wird möglicherweise unvollständig wiedergegeben.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Profil bearbeiten",
   "account.follow": "Folgen",
   "account.followers": "Folgende",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} ist umgezogen auf:",
   "account.mute": "@{name} stummschalten",
   "account.mute_notifications": "Benachrichtigungen von @{name} verbergen",
+  "account.muted": "Muted",
   "account.posts": "Beiträge",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "@{name} melden",
@@ -216,6 +219,7 @@
   "report.target": "{target} melden",
   "search.placeholder": "Suche",
   "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Thread stummschalten",
   "status.open": "Diesen Beitrag öffnen",
   "status.pin": "Im Profil anheften",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Teilen",
   "status.reblogged_by": "{name} teilte",
   "status.reply": "Antworten",
@@ -250,7 +255,6 @@
   "status.show_more": "Mehr anzeigen",
   "status.unmute_conversation": "Stummschaltung von Thread aufheben",
   "status.unpin": "Vom Profil lösen",
-  "tabs_bar.compose": "Schreiben",
   "tabs_bar.federated_timeline": "Föderation",
   "tabs_bar.home": "Startseite",
   "tabs_bar.local_timeline": "Lokal",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 52a82ff27..c8ebf4b87 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -275,6 +275,10 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Pinned toot",
+        "id": "status.pinned"
+      },
+      {
         "defaultMessage": "{name} boosted",
         "id": "status.reblogged_by"
       }
@@ -470,8 +474,24 @@
         "id": "account.requested"
       },
       {
+        "defaultMessage": "Unblock @{name}",
+        "id": "account.unblock"
+      },
+      {
         "defaultMessage": "Follows you",
         "id": "account.follows_you"
+      },
+      {
+        "defaultMessage": "Blocked",
+        "id": "account.blocked"
+      },
+      {
+        "defaultMessage": "Muted",
+        "id": "account.muted"
+      },
+      {
+        "defaultMessage": "Domain hidden",
+        "id": "account.domain_blocked"
       }
     ],
     "path": "app/javascript/mastodon/features/account/components/header.json"
@@ -684,6 +704,14 @@
         "id": "search.placeholder"
       },
       {
+        "defaultMessage": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+        "id": "search_popout.tips.full_text"
+      },
+      {
+        "defaultMessage": "Simple text returns matching display names, usernames and hashtags",
+        "id": "search_popout.tips.text"
+      },
+      {
         "defaultMessage": "Advanced search format",
         "id": "search_popout.search_format"
       },
@@ -698,10 +726,6 @@
       {
         "defaultMessage": "status",
         "id": "search_popout.tips.status"
-      },
-      {
-        "defaultMessage": "Simple text returns matching display names, usernames and hashtags",
-        "id": "search_popout.tips.text"
       }
     ],
     "path": "app/javascript/mastodon/features/compose/components/search.json"
@@ -1590,6 +1614,10 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Close",
+        "id": "lightbox.close"
+      },
+      {
         "defaultMessage": "Additional comments",
         "id": "report.placeholder"
       },
@@ -1619,10 +1647,6 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "Compose",
-        "id": "tabs_bar.compose"
-      },
-      {
         "defaultMessage": "Home",
         "id": "tabs_bar.home"
       },
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index d2d8f3995..e6e0b012b 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Block @{name}",
   "account.block_domain": "Hide everything from {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Edit profile",
   "account.follow": "Follow",
   "account.followers": "Followers",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} has moved to:",
   "account.mute": "Mute @{name}",
   "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
   "account.posts": "Toots",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Report @{name}",
@@ -223,6 +226,7 @@
   "report.target": "Reporting {target}",
   "search.placeholder": "Search",
   "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -245,6 +249,7 @@
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
   "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Boost",
   "status.reblogged_by": "{name} boosted",
   "status.reply": "Reply",
@@ -257,7 +262,6 @@
   "status.show_more": "Show more",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
-  "tabs_bar.compose": "Compose",
   "tabs_bar.federated_timeline": "Federated",
   "tabs_bar.home": "Home",
   "tabs_bar.local_timeline": "Local",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index b6153145f..0e5e22749 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Bloki @{name}",
   "account.block_domain": "Kaŝi ĉion de {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Subaj informoj povas reflekti la profilon de la uzanto nekomplete.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Redakti profilon",
   "account.follow": "Sekvi",
   "account.followers": "Sekvantoj",
@@ -13,8 +15,9 @@
   "account.moved_to": "{name} moviĝis al:",
   "account.mute": "Silentigi @{name}",
   "account.mute_notifications": "Silentigi sciigojn el @{name}",
-  "account.posts": "Hupoj",
-  "account.posts_with_replies": "Toots with replies",
+  "account.muted": "Muted",
+  "account.posts": "Mesaĝoj",
+  "account.posts_with_replies": "Mesaĝoj kun respondoj",
   "account.report": "Signali @{name}",
   "account.requested": "Atendo de aprobo. Alklaku por nuligi peton de sekvado",
   "account.share": "Diskonigi la profilon de @{name}",
@@ -147,7 +150,7 @@
   "navigation_bar.edit_profile": "Redakti profilon",
   "navigation_bar.favourites": "Stelumoj",
   "navigation_bar.follow_requests": "Petoj de sekvado",
-  "navigation_bar.info": "Pri ĉiu tiu nodo",
+  "navigation_bar.info": "Pri ĉi tiu nodo",
   "navigation_bar.keyboard_shortcuts": "Klavaraj mallongigoj",
   "navigation_bar.lists": "Listoj",
   "navigation_bar.logout": "Elsaluti",
@@ -208,21 +211,22 @@
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Nuligi",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.forward": "Plusendi al {target}",
+  "report.forward_hint": "La konto estas en alia servilo. Ĉu sendi sennomigitan kopion de la signalo ankaŭ tien?",
+  "report.hint": "La signalo estos sendita al la kontrolantoj de via nodo. Vi povas doni klarigon pri kial vi signalas ĉi tiun konton sube:",
   "report.placeholder": "Pliaj komentoj",
   "report.submit": "Sendi",
   "report.target": "Signali {target}",
   "search.placeholder": "Serĉi",
   "search_popout.search_format": "Detala serĉo",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "kradvorto",
   "search_popout.tips.status": "mesaĝoj",
   "search_popout.tips.text": "Simpla teksto montras la kongruajn afiŝitajn nomojn, uzantnomojn kaj kradvortojn",
   "search_popout.tips.user": "uzanto",
-  "search_results.accounts": "People",
-  "search_results.hashtags": "Hashtags",
-  "search_results.statuses": "Toots",
+  "search_results.accounts": "Homoj",
+  "search_results.hashtags": "Kradvortoj",
+  "search_results.statuses": "Mesaĝoj",
   "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezultoj}}",
   "standalone.public_title": "Enrigardo…",
   "status.block": "Bloki @{name}",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Silentigi konversacion",
   "status.open": "Grandigi ĉi tiun mesaĝon",
   "status.pin": "Alpingli en la profilo",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Diskonigi",
   "status.reblogged_by": "{name} diskonigis",
   "status.reply": "Respondi",
@@ -250,7 +255,6 @@
   "status.show_more": "Grandigi",
   "status.unmute_conversation": "Malsilentigi konversacion",
   "status.unpin": "Depingli de profilo",
-  "tabs_bar.compose": "Ekskribi",
   "tabs_bar.federated_timeline": "Fratara tempolinio",
   "tabs_bar.home": "Hejmo",
   "tabs_bar.local_timeline": "Loka tempolinio",
@@ -259,7 +263,7 @@
   "upload_area.title": "Altreni kaj lasi por alŝuti",
   "upload_button.label": "Aldoni aŭdovidaĵon",
   "upload_form.description": "Priskribi por misvidantaj homoj",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "Stuci",
   "upload_form.undo": "Malfari",
   "upload_progress.label": "Alŝutado…",
   "video.close": "Fermi videon",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 5f16ec974..d172dff1c 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Bloquear",
   "account.block_domain": "Ocultar todo de {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "La siguiente información del usuario puede estar incompleta.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} se ha mudado a:",
   "account.mute": "Silenciar a @{name}",
   "account.mute_notifications": "Silenciar notificaciones de @{name}",
+  "account.muted": "Muted",
   "account.posts": "Publicaciones",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Reportar a @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Reportando",
   "search.placeholder": "Buscar",
   "search_popout.search_format": "Formato de búsqueda avanzada",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "etiqueta",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "El texto simple devuelve correspondencias de nombre, usuario y hashtag",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Silenciar conversación",
   "status.open": "Expandir estado",
   "status.pin": "Fijar",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Retootear",
   "status.reblogged_by": "Retooteado por {name}",
   "status.reply": "Responder",
@@ -250,7 +255,6 @@
   "status.show_more": "Mostrar más",
   "status.unmute_conversation": "Dejar de silenciar conversación",
   "status.unpin": "Dejar de fijar",
-  "tabs_bar.compose": "Redactar",
   "tabs_bar.federated_timeline": "Federado",
   "tabs_bar.home": "Inicio",
   "tabs_bar.local_timeline": "Local",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 4fd467a66..b5b81bff9 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -1,7 +1,9 @@
 {
   "account.block": "مسدودسازی @{name}",
   "account.block_domain": "پنهان‌سازی همه چیز از سرور {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "اطلاعات زیر ممکن است نمایهٔ این کاربر را به تمامی نشان ندهد.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "ویرایش نمایه",
   "account.follow": "پی بگیرید",
   "account.followers": "پیگیران",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} منتقل شده است به:",
   "account.mute": "بی‌صدا کردن @{name}",
   "account.mute_notifications": "بی‌صداکردن اعلان‌ها از طرف @{name}",
+  "account.muted": "Muted",
   "account.posts": "نوشته‌ها",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "گزارش @{name}",
@@ -216,6 +219,7 @@
   "report.target": "گزارش‌دادن",
   "search.placeholder": "جستجو",
   "search_popout.search_format": "راهنمای جستجوی پیشرفته",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "هشتگ",
   "search_popout.tips.status": "نوشته",
   "search_popout.tips.text": "جستجوی متنی ساده برای نام‌ها، نام‌های کاربری، و هشتگ‌ها",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "بی‌صداکردن گفتگو",
   "status.open": "این نوشته را باز کن",
   "status.pin": "نوشتهٔ ثابت نمایه",
+  "status.pinned": "Pinned toot",
   "status.reblog": "بازبوقیدن",
   "status.reblogged_by": "‫{name}‬ بازبوقید",
   "status.reply": "پاسخ",
@@ -250,7 +255,6 @@
   "status.show_more": "نمایش",
   "status.unmute_conversation": "باصداکردن گفتگو",
   "status.unpin": "برداشتن نوشتهٔ ثابت نمایه",
-  "tabs_bar.compose": "بنویسید",
   "tabs_bar.federated_timeline": "همگانی",
   "tabs_bar.home": "خانه",
   "tabs_bar.local_timeline": "محلی",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index d27ba1834..aa97aae84 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Estä @{name}",
   "account.block_domain": "Piilota kaikki sisältö verkkotunnuksesta {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Alla olevat käyttäjän profiilitiedot saattavat olla epätäydellisiä.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Muokkaa",
   "account.follow": "Seuraa",
   "account.followers": "Seuraajia",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} on muuttanut instanssiin:",
   "account.mute": "Mykistä @{name}",
   "account.mute_notifications": "Mykistä ilmoitukset käyttäjältä @{name}",
+  "account.muted": "Muted",
   "account.posts": "Töötit",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Report @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Reporting",
   "search.placeholder": "Hae",
   "search_popout.search_format": "Tarkennettu haku",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtagi",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Pelkkä tekstihaku palauttaa hakua vastaavat nimimerkit, käyttäjänimet ja hastagit",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Mykistä keskustelu",
   "status.open": "Laajenna statuspäivitys",
   "status.pin": "Kiinnitä profiiliin",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Buustaa",
   "status.reblogged_by": "{name} buustasi",
   "status.reply": "Vastaa",
@@ -250,7 +255,6 @@
   "status.show_more": "Näytä lisää",
   "status.unmute_conversation": "Poista mykistys keskustelulta",
   "status.unpin": "Irrota profiilista",
-  "tabs_bar.compose": "Luo",
   "tabs_bar.federated_timeline": "Federated",
   "tabs_bar.home": "Koti",
   "tabs_bar.local_timeline": "Paikallinen",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index b10187dfd..77f4a0cca 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Bloquer @{name}",
   "account.block_domain": "Tout masquer venant de {domain}",
+  "account.blocked": "Bloqué",
   "account.disclaimer_full": "Les données ci-dessous peuvent ne pas refléter ce profil dans sa totalité.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Modifier le profil",
   "account.follow": "Suivre",
   "account.followers": "Abonné⋅e⋅s",
@@ -13,8 +15,9 @@
   "account.moved_to": "{name} a déménagé vers :",
   "account.mute": "Masquer @{name}",
   "account.mute_notifications": "Ignorer les notifications de @{name}",
-  "account.posts": "Statuts",
-  "account.posts_with_replies": "Toots with replies",
+  "account.muted": "Silencé",
+  "account.posts": "Pouets",
+  "account.posts_with_replies": "Pouets avec réponses",
   "account.report": "Signaler",
   "account.requested": "Invitation envoyée",
   "account.share": "Partager le profil de @{name}",
@@ -208,21 +211,22 @@
   "relative_time.minutes": "{number} min",
   "relative_time.seconds": "{number} s",
   "reply_indicator.cancel": "Annuler",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.forward": "Transférer à {target}",
+  "report.forward_hint": "Le compte provient d'un autre serveur. Envoyez également une copie anonyme du rapport ?",
+  "report.hint": "Le rapport sera envoyé aux modérateurs de votre instance. Vous pouvez expliquer pourquoi vous signalez ce compte ci-dessous :",
   "report.placeholder": "Commentaires additionnels",
   "report.submit": "Envoyer",
   "report.target": "Signalement",
   "search.placeholder": "Rechercher",
   "search_popout.search_format": "Recherche avancée",
+  "search_popout.tips.full_text": "Les textes simples retournent les pouets que vous avez écris, mis en favori, épinglés, ou ayant été mentionnés, ainsi que les noms d'utilisateurs, les noms affichés, et les hashtags correspondant.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "statuts",
   "search_popout.tips.text": "Un texte simple renvoie les noms affichés, les noms d’utilisateur⋅ice et les hashtags correspondants",
   "search_popout.tips.user": "utilisateur⋅ice",
-  "search_results.accounts": "People",
+  "search_results.accounts": "Personnes",
   "search_results.hashtags": "Hashtags",
-  "search_results.statuses": "Toots",
+  "search_results.statuses": "Pouets",
   "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
   "standalone.public_title": "Jeter un coup d’œil…",
   "status.block": "Block @{name}",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Masquer la conversation",
   "status.open": "Déplier ce statut",
   "status.pin": "Épingler sur le profil",
+  "status.pinned": "Pouet épinglé",
   "status.reblog": "Partager",
   "status.reblogged_by": "{name} a partagé :",
   "status.reply": "Répondre",
@@ -250,7 +255,6 @@
   "status.show_more": "Déplier",
   "status.unmute_conversation": "Ne plus masquer la conversation",
   "status.unpin": "Retirer du profil",
-  "tabs_bar.compose": "Composer",
   "tabs_bar.federated_timeline": "Fil public global",
   "tabs_bar.home": "Accueil",
   "tabs_bar.local_timeline": "Fil public local",
@@ -259,7 +263,7 @@
   "upload_area.title": "Glissez et déposez pour envoyer",
   "upload_button.label": "Joindre un média",
   "upload_form.description": "Décrire pour les malvoyants",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "Recadrer",
   "upload_form.undo": "Annuler",
   "upload_progress.label": "Envoi en cours…",
   "video.close": "Fermer la vidéo",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 0b91f57ae..e222ddaea 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Ocultar calquer contido de {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "A información inferior podería mostrar un perfil incompleto da usuaria.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidoras",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} marchou a:",
   "account.mute": "Acalar @{name}",
   "account.mute_notifications": "Acalar as notificacións de @{name}",
+  "account.muted": "Muted",
   "account.posts": "Toots",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Informar sobre @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Informar {target}",
   "search.placeholder": "Buscar",
   "search_popout.search_format": "Formato de busca avanzada",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "etiqueta",
   "search_popout.tips.status": "estado",
   "search_popout.tips.text": "Texto simple devolve coincidencias con nomes públicos, nomes de usuaria e etiquetas",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Acalar conversa",
   "status.open": "Expandir este estado",
   "status.pin": "Fixar no perfil",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Promover",
   "status.reblogged_by": "{name} promoveu",
   "status.reply": "Resposta",
@@ -250,7 +255,6 @@
   "status.show_more": "Mostrar máis",
   "status.unmute_conversation": "Non acalar a conversa",
   "status.unpin": "Despegar do perfil",
-  "tabs_bar.compose": "Compoñer",
   "tabs_bar.federated_timeline": "Federado",
   "tabs_bar.home": "Inicio",
   "tabs_bar.local_timeline": "Local",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index e430fabf8..b31976c42 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -1,7 +1,9 @@
 {
   "account.block": "חסימת @{name}",
   "account.block_domain": "להסתיר הכל מהקהילה {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "המידע להלן עשוי להיות לא עדכני או לא שלם.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "עריכת פרופיל",
   "account.follow": "מעקב",
   "account.followers": "עוקבים",
@@ -13,6 +15,7 @@
   "account.moved_to": "החשבון {name} הועבר אל:",
   "account.mute": "להשתיק את @{name}",
   "account.mute_notifications": "להסתיר התראות מאת @{name}",
+  "account.muted": "Muted",
   "account.posts": "הודעות",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "לדווח על @{name}",
@@ -216,6 +219,7 @@
   "report.target": "דיווח",
   "search.placeholder": "חיפוש",
   "search_popout.search_format": "מבנה חיפוש מתקדם",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "האשתג",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "טקסט פשוט מחזיר כינויים, שמות משתמש והאשתגים",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "השתקת שיחה",
   "status.open": "הרחבת הודעה",
   "status.pin": "לקבע באודות",
+  "status.pinned": "Pinned toot",
   "status.reblog": "הדהוד",
   "status.reblogged_by": "הודהד על ידי {name}",
   "status.reply": "תגובה",
@@ -250,7 +255,6 @@
   "status.show_more": "הראה יותר",
   "status.unmute_conversation": "הסרת השתקת שיחה",
   "status.unpin": "לשחרר מקיבוע באודות",
-  "tabs_bar.compose": "חיבור",
   "tabs_bar.federated_timeline": "ציר זמן בין-קהילתי",
   "tabs_bar.home": "בבית",
   "tabs_bar.local_timeline": "ציר זמן מקומי",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index 543a739bc..d176a5df6 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Blokiraj @{name}",
   "account.block_domain": "Sakrij sve sa {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Ovaj korisnik je sa druge instance. Ovaj broj bi mogao biti veći.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Uredi profil",
   "account.follow": "Slijedi",
   "account.followers": "Sljedbenici",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} has moved to:",
   "account.mute": "Utišaj @{name}",
   "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
   "account.posts": "Postovi",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Prijavi @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Prijavljivanje",
   "search.placeholder": "Traži",
   "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Utišaj razgovor",
   "status.open": "Proširi ovaj status",
   "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Podigni",
   "status.reblogged_by": "{name} je podigao",
   "status.reply": "Odgovori",
@@ -250,7 +255,6 @@
   "status.show_more": "Pokaži više",
   "status.unmute_conversation": "Poništi utišavanje razgovora",
   "status.unpin": "Unpin from profile",
-  "tabs_bar.compose": "Sastavi",
   "tabs_bar.federated_timeline": "Federalni",
   "tabs_bar.home": "Dom",
   "tabs_bar.local_timeline": "Lokalno",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 38b3698b6..a4d2091ef 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -1,7 +1,9 @@
 {
   "account.block": "@{name} letiltása",
   "account.block_domain": "Minden elrejtése innen: {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Az alul található információk hiányosan mutathatják be a felhasználót.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Profil szerkesztése",
   "account.follow": "Követés",
   "account.followers": "Követők",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} átköltözött:",
   "account.mute": "@{name} némítása",
   "account.mute_notifications": "@{name} értesítések némítása",
+  "account.muted": "Muted",
   "account.posts": "Státuszok",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "@{name} jelentése",
@@ -216,6 +219,7 @@
   "report.target": "Reporting",
   "search.placeholder": "Keresés",
   "search_popout.search_format": "Fejlett keresés",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Beszélgetés némítása",
   "status.open": "Státusz kinagyítása",
   "status.pin": "Kitűzés a profilra",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Reblog",
   "status.reblogged_by": "{name} reblogolta",
   "status.reply": "Válasz",
@@ -250,7 +255,6 @@
   "status.show_more": "Többet",
   "status.unmute_conversation": "Beszélgetés némításának elvonása",
   "status.unpin": "Kitűzés eltávolítása a profilról",
-  "tabs_bar.compose": "Összeállítás",
   "tabs_bar.federated_timeline": "Federált",
   "tabs_bar.home": "Kezdőlap",
   "tabs_bar.local_timeline": "Local",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 15d8f22e6..b5e9a2b5a 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Արգելափակել @{name}֊ին",
   "account.block_domain": "Թաքցնել ամենը հետեւյալ տիրույթից՝ {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Ներքոհիշյալը կարող է ոչ ամբողջությամբ արտացոլել օգտատիրոջ էջի տվյալները։",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Խմբագրել անձնական էջը",
   "account.follow": "Հետեւել",
   "account.followers": "Հետեւվողներ",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name}֊ը տեղափոխվել է՝",
   "account.mute": "Լռեցնել @{name}֊ին",
   "account.mute_notifications": "Անջատել ծանուցումները @{name}֊ից",
+  "account.muted": "Muted",
   "account.posts": "Գրառումներ",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Բողոքել @{name}֊ից",
@@ -216,6 +219,7 @@
   "report.target": "Բողոքել {target}֊ի մասին",
   "search.placeholder": "Փնտրել",
   "search_popout.search_format": "Փնտրելու առաջադեմ ձեւ",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "պիտակ",
   "search_popout.tips.status": "թութ",
   "search_popout.tips.text": "Հասարակ տեքստը կվերադարձնի համընկնող անուններ, օգտանուններ ու պիտակներ",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Լռեցնել խոսակցությունը",
   "status.open": "Ընդարձակել այս թութը",
   "status.pin": "Ամրացնել անձնական էջում",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Տարածել",
   "status.reblogged_by": "{name} տարածել է",
   "status.reply": "Պատասխանել",
@@ -250,7 +255,6 @@
   "status.show_more": "Ավելին",
   "status.unmute_conversation": "Ապալռեցնել խոսակցությունը",
   "status.unpin": "Հանել անձնական էջից",
-  "tabs_bar.compose": "Շարադրել",
   "tabs_bar.federated_timeline": "Դաշնային",
   "tabs_bar.home": "Հիմնական",
   "tabs_bar.local_timeline": "Տեղական",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index ac4aaa1aa..596415cde 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Blokir @{name}",
   "account.block_domain": "Hide everything from {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Ubah profil",
   "account.follow": "Ikuti",
   "account.followers": "Pengikut",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} has moved to:",
   "account.mute": "Bisukan @{name}",
   "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
   "account.posts": "Postingan",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Laporkan @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Melaporkan",
   "search.placeholder": "Pencarian",
   "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Mute conversation",
   "status.open": "Tampilkan status ini",
   "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Boost",
   "status.reblogged_by": "di-boost {name}",
   "status.reply": "Balas",
@@ -250,7 +255,6 @@
   "status.show_more": "Tampilkan semua",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
-  "tabs_bar.compose": "Tulis",
   "tabs_bar.federated_timeline": "Gabungan",
   "tabs_bar.home": "Beranda",
   "tabs_bar.local_timeline": "Lokal",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index d431fac49..4f554b08f 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Blokusar @{name}",
   "account.block_domain": "Hide everything from {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Modifikar profilo",
   "account.follow": "Sequar",
   "account.followers": "Sequanti",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} has moved to:",
   "account.mute": "Celar @{name}",
   "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
   "account.posts": "Mesaji",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Denuncar @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Denuncante",
   "search.placeholder": "Serchez",
   "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Mute conversation",
   "status.open": "Detaligar ca mesajo",
   "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Repetar",
   "status.reblogged_by": "{name} repetita",
   "status.reply": "Respondar",
@@ -250,7 +255,6 @@
   "status.show_more": "Montrar plue",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
-  "tabs_bar.compose": "Kompozar",
   "tabs_bar.federated_timeline": "Federata",
   "tabs_bar.home": "Hemo",
   "tabs_bar.local_timeline": "Lokala",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index eec1a7feb..6b2532512 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Blocca @{name}",
   "account.block_domain": "Hide everything from {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Modifica profilo",
   "account.follow": "Segui",
   "account.followers": "Seguaci",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} has moved to:",
   "account.mute": "Silenzia @{name}",
   "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
   "account.posts": "Posts",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Segnala @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Invio la segnalazione",
   "search.placeholder": "Cerca",
   "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Mute conversation",
   "status.open": "Espandi questo post",
   "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Condividi",
   "status.reblogged_by": "{name} ha condiviso",
   "status.reply": "Rispondi",
@@ -250,7 +255,6 @@
   "status.show_more": "Mostra di più",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
-  "tabs_bar.compose": "Scrivi",
   "tabs_bar.federated_timeline": "Federazione",
   "tabs_bar.home": "Home",
   "tabs_bar.local_timeline": "Locale",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index f1ad2911d..04374abc3 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -1,7 +1,9 @@
 {
   "account.block": "@{name}さんをブロック",
   "account.block_domain": "{domain}全体を非表示",
+  "account.blocked": "ブロック済み",
   "account.disclaimer_full": "以下の情報は不正確な可能性があります。",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "プロフィールを編集",
   "account.follow": "フォロー",
   "account.followers": "フォロワー",
@@ -13,8 +15,9 @@
   "account.moved_to": "{name}さんは引っ越しました:",
   "account.mute": "@{name}さんをミュート",
   "account.mute_notifications": "@{name}さんからの通知を受け取らない",
+  "account.muted": "ミュート済み",
   "account.posts": "投稿",
-  "account.posts_with_replies": "トゥートと返信",
+  "account.posts_with_replies": "投稿と返信",
   "account.report": "@{name}さんを通報",
   "account.requested": "フォロー承認待ちです。クリックしてキャンセル",
   "account.share": "@{name}さんのプロフィールを共有する",
@@ -223,6 +226,7 @@
   "report.target": "{target}さんを通報する",
   "search.placeholder": "検索",
   "search_popout.search_format": "高度な検索フォーマット",
+  "search_popout.tips.full_text": "表示名やユーザー名、ハッシュタグのほか、あなたのトゥートやお気に入り、ブーストしたトゥート、返信に一致する単純なテキスト。",
   "search_popout.tips.hashtag": "ハッシュタグ",
   "search_popout.tips.status": "トゥート",
   "search_popout.tips.text": "表示名やユーザー名、ハッシュタグに一致する単純なテキスト",
@@ -245,8 +249,9 @@
   "status.mute_conversation": "会話をミュート",
   "status.open": "詳細を表示",
   "status.pin": "プロフィールに固定表示",
+  "status.pinned": "固定されたトゥート",
   "status.reblog": "ブースト",
-  "status.reblogged_by": "{name}さんにブーストされました",
+  "status.reblogged_by": "{name}さんがブースト",
   "status.reply": "返信",
   "status.replyAll": "全員に返信",
   "status.report": "@{name}さんを通報",
@@ -257,12 +262,11 @@
   "status.show_more": "もっと見る",
   "status.unmute_conversation": "会話のミュートを解除",
   "status.unpin": "プロフィールの固定表示を解除",
-  "tabs_bar.compose": "投稿",
   "tabs_bar.federated_timeline": "連合",
   "tabs_bar.home": "ホーム",
   "tabs_bar.local_timeline": "ローカル",
   "tabs_bar.notifications": "通知",
-  "ui.beforeunload": "Mastodonから離れるとあなたのドラフトは失われます。",
+  "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 8e7c82682..9f7cebf6b 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -1,7 +1,9 @@
 {
   "account.block": "@{name}을 차단",
   "account.block_domain": "{domain} 전체를 숨김",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "여기 있는 정보는 유저의 프로파일을 정확히 반영하지 못 할 수도 있습니다.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "프로필 편집",
   "account.follow": "팔로우",
   "account.followers": "팔로워",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name}는 계정을 이동했습니다:",
   "account.mute": "@{name} 뮤트",
   "account.mute_notifications": "@{name}의 알림을 뮤트",
+  "account.muted": "Muted",
   "account.posts": "게시물",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "@{name} 신고",
@@ -216,6 +219,7 @@
   "report.target": "문제가 된 사용자",
   "search.placeholder": "검색",
   "search_popout.search_format": "고급 검색 방법",
+  "search_popout.tips.full_text": "단순한 텍스트 검색은 당신이 작성했거나, 관심글로 지정했거나, 부스트했거나, 멘션을 받은 게시글, 그리고 유저네임, 디스플레이네임, 해시태그를 반환합니다.",
   "search_popout.tips.hashtag": "해시태그",
   "search_popout.tips.status": "툿",
   "search_popout.tips.text": "단순한 텍스트 검색은 관계된 프로필 이름, 유저 이름 그리고 해시태그를 표시합니다",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "이 대화를 뮤트",
   "status.open": "상세 정보 표시",
   "status.pin": "고정",
+  "status.pinned": "Pinned toot",
   "status.reblog": "부스트",
   "status.reblogged_by": "{name}님이 부스트 했습니다",
   "status.reply": "답장",
@@ -250,7 +255,6 @@
   "status.show_more": "더 보기",
   "status.unmute_conversation": "이 대화의 뮤트 해제하기",
   "status.unpin": "고정 해제",
-  "tabs_bar.compose": "포스트",
   "tabs_bar.federated_timeline": "연합",
   "tabs_bar.home": "홈",
   "tabs_bar.local_timeline": "로컬",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 315f92120..ccf3cefc2 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Blokkeer @{name}",
   "account.block_domain": "Negeer alles van {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "De informatie hieronder kan mogelijk een incompleet beeld geven van dit gebruikersprofiel.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Profiel bewerken",
   "account.follow": "Volgen",
   "account.followers": "Volgers",
@@ -13,8 +15,9 @@
   "account.moved_to": "{name} is verhuisd naar:",
   "account.mute": "Negeer @{name}",
   "account.mute_notifications": "Negeer meldingen van @{name}",
+  "account.muted": "Muted",
   "account.posts": "Toots",
-  "account.posts_with_replies": "Toots with replies",
+  "account.posts_with_replies": "Toots met reacties",
   "account.report": "Rapporteer @{name}",
   "account.requested": "Wacht op goedkeuring. Klik om het volgverzoek te annuleren",
   "account.share": "Profiel van @{name} delen",
@@ -208,19 +211,20 @@
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Annuleren",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.forward": "Doorsturen naar {target}",
+  "report.forward_hint": "Het account bevindt zich op een andere server. Stuur daar eveneens een geanonimiseerde kopie van de gerapporteerde toot(s) naartoe?",
+  "report.hint": "De gerapporteerde toot(s) worden naar de moderatoren van  jouw server gestuurd. Je kunt hieronder een uitleg geven waarom je dit account rapporteert:",
   "report.placeholder": "Extra opmerkingen",
   "report.submit": "Verzenden",
   "report.target": "Rapporteer {target}",
   "search.placeholder": "Zoeken",
   "search_popout.search_format": "Geavanceerd zoeken",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "toot",
   "search_popout.tips.text": "Gebruik gewone tekst om te zoeken op weergavenamen, gebruikersnamen en hashtags",
   "search_popout.tips.user": "gebruiker",
-  "search_results.accounts": "People",
+  "search_results.accounts": "Gebruikers",
   "search_results.hashtags": "Hashtags",
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Negeer conversatie",
   "status.open": "Toot volledig tonen",
   "status.pin": "Aan profielpagina vastmaken",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Boost",
   "status.reblogged_by": "{name} boostte",
   "status.reply": "Reageren",
@@ -250,7 +255,6 @@
   "status.show_more": "Meer tonen",
   "status.unmute_conversation": "Conversatie niet meer negeren",
   "status.unpin": "Van profielpagina losmaken",
-  "tabs_bar.compose": "Schrijven",
   "tabs_bar.federated_timeline": "Globaal",
   "tabs_bar.home": "Start",
   "tabs_bar.local_timeline": "Lokaal",
@@ -259,7 +263,7 @@
   "upload_area.title": "Hierin slepen om te uploaden",
   "upload_button.label": "Media toevoegen",
   "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "Bijsnijden",
   "upload_form.undo": "Ongedaan maken",
   "upload_progress.label": "Uploaden...",
   "video.close": "Video sluiten",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index b319ea1a5..b7ceb9f73 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Blokkér @{name}",
   "account.block_domain": "Skjul alt fra {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Informasjonen nedenfor kan gi et ufullstendig bilde av brukerens profil.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Rediger profil",
   "account.follow": "Følg",
   "account.followers": "Følgere",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} har flyttet til:",
   "account.mute": "Demp @{name}",
   "account.mute_notifications": "Ignorer varsler fra @{name}",
+  "account.muted": "Muted",
   "account.posts": "Innlegg",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Rapportér @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Rapporterer",
   "search.placeholder": "Søk",
   "search_popout.search_format": "Avansert søkeformat",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "emneknagg",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Enkel tekst returnerer matchende visningsnavn, brukernavn og emneknagger",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Demp samtale",
   "status.open": "Utvid denne statusen",
   "status.pin": "Fest på profilen",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Fremhev",
   "status.reblogged_by": "Fremhevd av {name}",
   "status.reply": "Svar",
@@ -250,7 +255,6 @@
   "status.show_more": "Vis mer",
   "status.unmute_conversation": "Ikke demp samtale",
   "status.unpin": "Angre festing på profilen",
-  "tabs_bar.compose": "Komponer",
   "tabs_bar.federated_timeline": "Felles",
   "tabs_bar.home": "Hjem",
   "tabs_bar.local_timeline": "Lokal",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 2402e1759..c9a15c751 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Blocar @{name}",
   "account.block_domain": "Tot amagar del domeni {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Aquelas informacions de perfil pòdon èsser incomplètas.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Modificar lo perfil",
   "account.follow": "Sègre",
   "account.followers": "Seguidors",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} a mudat los catons a :",
   "account.mute": "Rescondre @{name}",
   "account.mute_notifications": "Rescondre las notificacions de @{name}",
+  "account.muted": "Muted",
   "account.posts": "Estatuts",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Senhalar @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Senhalar {target}",
   "search.placeholder": "Recercar",
   "search_popout.search_format": "Format recèrca avançada",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "etiqueta",
   "search_popout.tips.status": "estatut",
   "search_popout.tips.text": "Lo tèxt brut tòrna escais, noms d’utilizaire e etiquetas correspondents",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Rescondre la conversacion",
   "status.open": "Desplegar aqueste estatut",
   "status.pin": "Penjar al perfil",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Partejar",
   "status.reblogged_by": "{name} a partejat",
   "status.reply": "Respondre",
@@ -250,7 +255,6 @@
   "status.show_more": "Desplegar",
   "status.unmute_conversation": "Tornar mostrar la conversacion",
   "status.unpin": "Tirar del perfil",
-  "tabs_bar.compose": "Compausar",
   "tabs_bar.federated_timeline": "Flux public global",
   "tabs_bar.home": "Acuèlh",
   "tabs_bar.local_timeline": "Flux public local",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 4eccc3655..39db3a338 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Blokuj @{name}",
   "account.block_domain": "Blokuj wszystko z {domain}",
+  "account.blocked": "Zablokowany",
   "account.disclaimer_full": "Poniższe informacje mogą nie odwzorowywać bezbłędnie profilu użytkownika.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Edytuj profil",
   "account.follow": "Śledź",
   "account.followers": "Śledzący",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} przeniósł się do:",
   "account.mute": "Wycisz @{name}",
   "account.mute_notifications": "Wycisz powiadomienia o @{name}",
+  "account.muted": "Wyciszony",
   "account.posts": "Wpisy",
   "account.posts_with_replies": "Wpisy z odpowiedziami",
   "account.report": "Zgłoś @{name}",
@@ -223,6 +226,7 @@
   "report.target": "Zgłaszanie {target}",
   "search.placeholder": "Szukaj",
   "search_popout.search_format": "Zaawansowane wyszukiwanie",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "wpis",
   "search_popout.tips.text": "Proste wyszukiwanie pasujących pseudonimów, nazw użytkowników i hashtagów",
@@ -245,6 +249,7 @@
   "status.mute_conversation": "Wycisz konwersację",
   "status.open": "Rozszerz ten wpis",
   "status.pin": "Przypnij do profilu",
+  "status.pinned": "Przypięty wpis",
   "status.reblog": "Podbij",
   "status.reblogged_by": "{name} podbił",
   "status.reply": "Odpowiedz",
@@ -257,7 +262,6 @@
   "status.show_more": "Pokaż więcej",
   "status.unmute_conversation": "Cofnij wyciszenie konwersacji",
   "status.unpin": "Odepnij z profilu",
-  "tabs_bar.compose": "Napisz",
   "tabs_bar.federated_timeline": "Globalne",
   "tabs_bar.home": "Strona główna",
   "tabs_bar.local_timeline": "Lokalne",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index c07661e92..fa36bb890 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Esconder tudo de {domain}",
+  "account.blocked": "Bloqueado",
   "account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de maneira incompleta.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
@@ -13,8 +15,9 @@
   "account.moved_to": "{name} se mudou para:",
   "account.mute": "Silenciar @{name}",
   "account.mute_notifications": "Silenciar notificações de @{name}",
-  "account.posts": "Posts",
-  "account.posts_with_replies": "Toots with replies",
+  "account.muted": "Silenciado",
+  "account.posts": "Toots",
+  "account.posts_with_replies": "Toots e respostas",
   "account.report": "Denunciar @{name}",
   "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação",
   "account.share": "Compartilhar perfil de @{name}",
@@ -208,19 +211,20 @@
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Cancelar",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.forward": "Encaminhar para {target}",
+  "report.forward_hint": "Essa conta pertence à um outro servidor. Encaminhar uma cópia da denúncia com seus dados tornados anônimos para esse servidor?",
+  "report.hint": "A sua denúncia será enviada aos moderadores da instância. Você pode adicionar uma explicação de porque você está denunciando essa conta abaixo:",
   "report.placeholder": "Comentários adicionais",
   "report.submit": "Enviar",
   "report.target": "Denunciar",
   "search.placeholder": "Pesquisar",
   "search_popout.search_format": "Formato de busca avançado",
+  "search_popout.tips.full_text": "Texto simples retorna status que você escreveu, favoritou, compartilhou ou em que tenha sido mencionado; também retorna nomes de exibição, usuários e hashtags correspondentes.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Texto simples retorna nomes de exibição, usuários e hashtags correspondentes",
   "search_popout.tips.user": "usuário",
-  "search_results.accounts": "People",
+  "search_results.accounts": "Pessoas",
   "search_results.hashtags": "Hashtags",
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Silenciar conversa",
   "status.open": "Expandir",
   "status.pin": "Fixar no perfil",
+  "status.pinned": "Toot fixado",
   "status.reblog": "Compartilhar",
   "status.reblogged_by": "{name} compartilhou",
   "status.reply": "Responder",
@@ -250,7 +255,6 @@
   "status.show_more": "Mostrar mais",
   "status.unmute_conversation": "Desativar silêncio desta conversa",
   "status.unpin": "Desafixar do perfil",
-  "tabs_bar.compose": "Criar",
   "tabs_bar.federated_timeline": "Global",
   "tabs_bar.home": "Página inicial",
   "tabs_bar.local_timeline": "Local",
@@ -259,7 +263,7 @@
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar mídia",
   "upload_form.description": "Descreva a imagem para deficientes visuais",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "Recortar",
   "upload_form.undo": "Desfazer",
   "upload_progress.label": "Salvando...",
   "video.close": "Fechar vídeo",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 61233a1b9..f059e7c20 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Esconder tudo do domínio {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de forma incompleta.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} mudou a sua conta para:",
   "account.mute": "Silenciar @{name}",
   "account.mute_notifications": "Silenciar notificações de @{name}",
+  "account.muted": "Muted",
   "account.posts": "Posts",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Denunciar @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Denunciar",
   "search.placeholder": "Pesquisar",
   "search_popout.search_format": "Formato avançado de pesquisa",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Silenciar conversa",
   "status.open": "Expandir",
   "status.pin": "Fixar no perfil",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Partilhar",
   "status.reblogged_by": "{name} partilhou",
   "status.reply": "Responder",
@@ -250,7 +255,6 @@
   "status.show_more": "Mostrar mais",
   "status.unmute_conversation": "Deixar de silenciar esta conversa",
   "status.unpin": "Não fixar no perfil",
-  "tabs_bar.compose": "Criar",
   "tabs_bar.federated_timeline": "Global",
   "tabs_bar.home": "Home",
   "tabs_bar.local_timeline": "Local",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 76d3fec13..06a7d732b 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Блокировать",
   "account.block_domain": "Блокировать все с {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Нижеуказанная информация может не полностью отражать профиль пользователя.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Изменить профиль",
   "account.follow": "Подписаться",
   "account.followers": "Подписаны",
@@ -13,6 +15,7 @@
   "account.moved_to": "Ищите {name} здесь:",
   "account.mute": "Заглушить",
   "account.mute_notifications": "Скрыть уведомления от @{name}",
+  "account.muted": "Muted",
   "account.posts": "Посты",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Пожаловаться",
@@ -216,6 +219,7 @@
   "report.target": "Жалуемся на",
   "search.placeholder": "Поиск",
   "search_popout.search_format": "Продвинутый формат поиска",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "хэштег",
   "search_popout.tips.status": "статус",
   "search_popout.tips.text": "Простой ввод текста покажет совпадающие имена пользователей, отображаемые имена и хэштеги",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Заглушить тред",
   "status.open": "Развернуть статус",
   "status.pin": "Закрепить в профиле",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Продвинуть",
   "status.reblogged_by": "{name} продвинул(а)",
   "status.reply": "Ответить",
@@ -250,7 +255,6 @@
   "status.show_more": "Развернуть",
   "status.unmute_conversation": "Снять глушение с треда",
   "status.unpin": "Открепить от профиля",
-  "tabs_bar.compose": "Написать",
   "tabs_bar.federated_timeline": "Глобальная",
   "tabs_bar.home": "Главная",
   "tabs_bar.local_timeline": "Локальная",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index de273770a..b76753899 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -1,20 +1,23 @@
 {
   "account.block": "Blokovať @{name}",
   "account.block_domain": "Ukryť všetko z {domain}",
+  "account.blocked": "Blokovaný/á",
   "account.disclaimer_full": "Inofrmácie nižšie nemusia byť úplným odrazom uživateľovho účtu.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Upraviť profil",
   "account.follow": "Následovať",
   "account.followers": "Sledujúci",
   "account.follows": "Sledujete",
-  "account.follows_you": "Následuje vás",
+  "account.follows_you": "Následuje ťa",
   "account.hide_reblogs": "Skryť povýšenia od @{name}",
   "account.media": "Médiá",
   "account.mention": "Spomeňte @{name}",
   "account.moved_to": "{name} sa presunul/a na:",
   "account.mute": "Ignorovať @{name}",
   "account.mute_notifications": "Stĺmiť notifikácie od @{name}",
+  "account.muted": "Utíšený/á",
   "account.posts": "Hlášky",
-  "account.posts_with_replies": "Toots with replies",
+  "account.posts_with_replies": "Príspevky s odpoveďami",
   "account.report": "Nahlásiť @{name}",
   "account.requested": "Čaká na schválenie. Kliknite pre zrušenie žiadosti",
   "account.share": "Zdieľať @{name} profil",
@@ -156,8 +159,8 @@
   "navigation_bar.preferences": "Možnosti",
   "navigation_bar.public_timeline": "Federovaná časová os",
   "notification.favourite": "{name} sa páči tvoj status",
-  "notification.follow": "{name} vás začal(a) sledovať",
-  "notification.mention": "{name} vás spomenul",
+  "notification.follow": "{name} ťa začal/a následovať",
+  "notification.mention": "{name} ťa spomenul/a",
   "notification.reblog": "{name} re-tootol tvoj status",
   "notifications.clear": "Vyčistiť zoznam notifikácii",
   "notifications.clear_confirmation": "Naozaj chcete nenávratne prečistiť všetky vaše notifikácie?",
@@ -208,21 +211,22 @@
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Zrušiť",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.forward": "Posuň ku {target}",
+  "report.forward_hint": "Tento účet je z iného serveru. Chceš poslať anonymnú kópiu reportu aj tam?",
+  "report.hint": "Toto nahlásenie bude zaslané správcom servera. Môžeš napísať odvôvodnenie prečo si nahlásil/a tento účet:",
   "report.placeholder": "Ďalšie komentáre",
   "report.submit": "Poslať",
   "report.target": "Nahlásenie {target}",
   "search.placeholder": "Hľadať",
   "search_popout.search_format": "Pokročilý formát vyhľadávania",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "haštag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Jednoduchý text vráti zhodujúce sa mená, prezývky a hashtagy",
   "search_popout.tips.user": "používateľ",
-  "search_results.accounts": "People",
-  "search_results.hashtags": "Hashtags",
-  "search_results.statuses": "Toots",
+  "search_results.accounts": "Ľudia",
+  "search_results.hashtags": "Haštagy",
+  "search_results.statuses": "Príspevky",
   "search_results.total": "{count, number} {count, plural, one {result} ostatné {results}}",
   "standalone.public_title": "Pohľad dovnútra...",
   "status.block": "Blokovať @{name}",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Ignorovať konverzáciu",
   "status.open": "Otvoriť tento status",
   "status.pin": "Pripnúť na profil",
+  "status.pinned": "Pripnutý príspevok",
   "status.reblog": "Povýšiť",
   "status.reblogged_by": "{name} povýšil",
   "status.reply": "Odpovedať",
@@ -250,7 +255,6 @@
   "status.show_more": "Zobraz viac",
   "status.unmute_conversation": "Prestať ignorovať konverzáciu",
   "status.unpin": "Odopnúť z profilu",
-  "tabs_bar.compose": "Napísať",
   "tabs_bar.federated_timeline": "Federovaná",
   "tabs_bar.home": "Domov",
   "tabs_bar.local_timeline": "Lokálna",
@@ -259,7 +263,7 @@
   "upload_area.title": "Ťahaj a pusti pre nahratie",
   "upload_button.label": "Pridať médiá",
   "upload_form.description": "Opis pre slabo vidiacich",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "Vystrihni",
   "upload_form.undo": "Navrátiť",
   "upload_progress.label": "Nahráva sa...",
   "video.close": "Zavrieť video",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index bf67a52d6..a672ae6ca 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Blokiraj korisnika @{name}",
   "account.block_domain": "Sakrij sve sa domena {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Navedene informacije možda ne odslikavaju korisnički profil u potpunosti.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Izmeni profil",
   "account.follow": "Zaprati",
   "account.followers": "Pratioca",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} se pomerio na:",
   "account.mute": "Ućutkaj korisnika @{name}",
   "account.mute_notifications": "Isključi obaveštenja od korisnika @{name}",
+  "account.muted": "Muted",
   "account.posts": "Statusa",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Prijavi @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Prijavljujem {target}",
   "search.placeholder": "Pretraga",
   "search_popout.search_format": "Napredni format pretrage",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hešteg",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Traženjem običnog teksta ćete dobiti sva pronađena imena, sva korisnička imena i sve nađene heštegove",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Ućutkaj prepisku",
   "status.open": "Proširi ovaj status",
   "status.pin": "Prikači na profil",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Podrži",
   "status.reblogged_by": "{name} podržao(la)",
   "status.reply": "Odgovori",
@@ -250,7 +255,6 @@
   "status.show_more": "Prikaži više",
   "status.unmute_conversation": "Uključi prepisku",
   "status.unpin": "Otkači sa profila",
-  "tabs_bar.compose": "Napiši",
   "tabs_bar.federated_timeline": "Federisano",
   "tabs_bar.home": "Početna",
   "tabs_bar.local_timeline": "Lokalno",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index d8d1ebae7..1e3a3ce2b 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Блокирај корисника @{name}",
   "account.block_domain": "Сакриј све са домена {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Наведене информације можда не одсликавају кориснички профил у потпуности.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Измени профил",
   "account.follow": "Запрати",
   "account.followers": "Пратиоца",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} се померио на:",
   "account.mute": "Ућуткај корисника @{name}",
   "account.mute_notifications": "Искључи обавештења од корисника @{name}",
+  "account.muted": "Muted",
   "account.posts": "Статуса",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Пријави @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Пријављујем {target}",
   "search.placeholder": "Претрага",
   "search_popout.search_format": "Напредни формат претраге",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "хештег",
   "search_popout.tips.status": "статус",
   "search_popout.tips.text": "Тражењем обичног текста ћете добити сва пронађена имена, сва корисничка имена и све нађене хештегове",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Ућуткај преписку",
   "status.open": "Прошири овај статус",
   "status.pin": "Прикачи на профил",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Подржи",
   "status.reblogged_by": "{name} подржао(ла)",
   "status.reply": "Одговори",
@@ -250,7 +255,6 @@
   "status.show_more": "Прикажи више",
   "status.unmute_conversation": "Укључи преписку",
   "status.unpin": "Откачи са профила",
-  "tabs_bar.compose": "Напиши",
   "tabs_bar.federated_timeline": "Федерисано",
   "tabs_bar.home": "Почетна",
   "tabs_bar.local_timeline": "Локално",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 0bf66c547..9c51d5b36 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Blockera @{name}",
   "account.block_domain": "Dölj allt från {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Informationen nedan kan spegla användarens profil ofullständigt.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Redigera profil",
   "account.follow": "Följ",
   "account.followers": "Följare",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} har flyttat till:",
   "account.mute": "Tysta @{name}",
   "account.mute_notifications": "Stäng av notifieringar från @{name}",
+  "account.muted": "Muted",
   "account.posts": "Inlägg",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Rapportera @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Rapporterar {target}",
   "search.placeholder": "Sök",
   "search_popout.search_format": "Avancerat sökformat",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Enkel text returnerar matchande visningsnamn, användarnamn och hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Tysta konversation",
   "status.open": "Utvidga denna status",
   "status.pin": "Fäst i profil",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Knuff",
   "status.reblogged_by": "{name} knuffade",
   "status.reply": "Svara",
@@ -250,7 +255,6 @@
   "status.show_more": "Visa mer",
   "status.unmute_conversation": "Öppna konversation",
   "status.unpin": "Ångra fäst i profil",
-  "tabs_bar.compose": "Skriv",
   "tabs_bar.federated_timeline": "Förenad",
   "tabs_bar.home": "Hem",
   "tabs_bar.local_timeline": "Lokal",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index d6ccc9412..cab2ce089 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Block @{name}",
   "account.block_domain": "Hide everything from {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Edit profile",
   "account.follow": "Follow",
   "account.followers": "Followers",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} has moved to:",
   "account.mute": "Mute @{name}",
   "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
   "account.posts": "Posts",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Report @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Reporting",
   "search.placeholder": "Search",
   "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Mute conversation",
   "status.open": "Expand this status",
   "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Boost",
   "status.reblogged_by": "{name} boosted",
   "status.reply": "Reply",
@@ -250,7 +255,6 @@
   "status.show_more": "Show more",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
-  "tabs_bar.compose": "Compose",
   "tabs_bar.federated_timeline": "Federated",
   "tabs_bar.home": "Home",
   "tabs_bar.local_timeline": "Local",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 702c8454d..83c10de34 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Engelle @{name}",
   "account.block_domain": "Hide everything from {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Profili düzenle",
   "account.follow": "Takip et",
   "account.followers": "Takipçiler",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} has moved to:",
   "account.mute": "Sustur @{name}",
   "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
   "account.posts": "Gönderiler",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Rapor et @{name}",
@@ -216,6 +219,7 @@
   "report.target": "Raporlama",
   "search.placeholder": "Ara",
   "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Mute conversation",
   "status.open": "Bu gönderiyi genişlet",
   "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Boost'la",
   "status.reblogged_by": "{name} boost etti",
   "status.reply": "Cevapla",
@@ -250,7 +255,6 @@
   "status.show_more": "Daha fazlası",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
-  "tabs_bar.compose": "Oluştur",
   "tabs_bar.federated_timeline": "Federe",
   "tabs_bar.home": "Ana sayfa",
   "tabs_bar.local_timeline": "Yerel",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index b5bd88cbb..b49f707e1 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -1,7 +1,9 @@
 {
   "account.block": "Заблокувати",
   "account.block_domain": "Заглушити {domain}",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Налаштування профілю",
   "account.follow": "Підписатися",
   "account.followers": "Підписники",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} has moved to:",
   "account.mute": "Заглушити",
   "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
   "account.posts": "Пости",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "Поскаржитися",
@@ -216,6 +219,7 @@
   "report.target": "Скаржимося на",
   "search.placeholder": "Пошук",
   "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "Заглушити діалог",
   "status.open": "Розгорнути допис",
   "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
   "status.reblog": "Передмухнути",
   "status.reblogged_by": "{name} передмухнув(-ла)",
   "status.reply": "Відповісти",
@@ -250,7 +255,6 @@
   "status.show_more": "Розгорнути",
   "status.unmute_conversation": "Зняти глушення з діалогу",
   "status.unpin": "Unpin from profile",
-  "tabs_bar.compose": "Написати",
   "tabs_bar.federated_timeline": "Глобальна",
   "tabs_bar.home": "Головна",
   "tabs_bar.local_timeline": "Локальна",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 7cc5903cd..5ccfbc4f4 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -1,7 +1,9 @@
 {
   "account.block": "屏蔽 @{name}",
   "account.block_domain": "隐藏来自 {domain} 的内容",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "此处显示的信息可能不是全部内容。",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "修改个人资料",
   "account.follow": "关注",
   "account.followers": "关注者",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} 已经迁移到:",
   "account.mute": "隐藏 @{name}",
   "account.mute_notifications": "隐藏来自 @{name} 的通知",
+  "account.muted": "Muted",
   "account.posts": "嘟文",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "举报 @{name}",
@@ -216,6 +219,7 @@
   "report.target": "举报 {target}",
   "search.placeholder": "搜索",
   "search_popout.search_format": "高级搜索格式",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "话题标签",
   "search_popout.tips.status": "嘟文",
   "search_popout.tips.text": "使用普通字符进行搜索将会返回昵称、用户名和话题标签",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "隐藏此对话",
   "status.open": "展开嘟文",
   "status.pin": "在个人资料页面置顶",
+  "status.pinned": "Pinned toot",
   "status.reblog": "转嘟",
   "status.reblogged_by": "{name} 转嘟了",
   "status.reply": "回复",
@@ -250,7 +255,6 @@
   "status.show_more": "显示内容",
   "status.unmute_conversation": "不再隐藏此对话",
   "status.unpin": "在个人资料页面取消置顶",
-  "tabs_bar.compose": "撰写",
   "tabs_bar.federated_timeline": "跨站",
   "tabs_bar.home": "主页",
   "tabs_bar.local_timeline": "本站",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index d21ecc463..b105fe426 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -1,7 +1,9 @@
 {
   "account.block": "封鎖 @{name}",
   "account.block_domain": "隱藏來自 {domain} 的一切文章",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "下列資料不一定完整。",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "修改個人資料",
   "account.follow": "關注",
   "account.followers": "關注的人",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} has moved to:",
   "account.mute": "將 @{name} 靜音",
   "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
   "account.posts": "文章",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "舉報 @{name}",
@@ -216,6 +219,7 @@
   "report.target": "舉報",
   "search.placeholder": "搜尋",
   "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "靜音對話",
   "status.open": "展開文章",
   "status.pin": "置頂到資料頁",
+  "status.pinned": "Pinned toot",
   "status.reblog": "轉推",
   "status.reblogged_by": "{name} 轉推",
   "status.reply": "回應",
@@ -250,7 +255,6 @@
   "status.show_more": "顯示更多",
   "status.unmute_conversation": "解禁對話",
   "status.unpin": "解除置頂",
-  "tabs_bar.compose": "撰寫",
   "tabs_bar.federated_timeline": "跨站",
   "tabs_bar.home": "主頁",
   "tabs_bar.local_timeline": "本站",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index f1ae29283..b0a94f67b 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -1,7 +1,9 @@
 {
   "account.block": "封鎖 @{name}",
   "account.block_domain": "隱藏來自 {domain} 的一切貼文",
+  "account.blocked": "Blocked",
   "account.disclaimer_full": "下列資料不一定完整。",
+  "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "編輯用者資訊",
   "account.follow": "關注",
   "account.followers": "專注者",
@@ -13,6 +15,7 @@
   "account.moved_to": "{name} has moved to:",
   "account.mute": "消音 @{name}",
   "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
   "account.posts": "貼文",
   "account.posts_with_replies": "Toots with replies",
   "account.report": "檢舉 @{name}",
@@ -216,6 +219,7 @@
   "report.target": "通報中",
   "search.placeholder": "搜尋",
   "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@@ -238,6 +242,7 @@
   "status.mute_conversation": "消音對話",
   "status.open": "展開這個狀態",
   "status.pin": "置頂到個人資訊頁",
+  "status.pinned": "Pinned toot",
   "status.reblog": "轉推",
   "status.reblogged_by": "{name} 轉推了",
   "status.reply": "回應",
@@ -250,7 +255,6 @@
   "status.show_more": "看更多",
   "status.unmute_conversation": "不消音對話",
   "status.unpin": "解除置頂",
-  "tabs_bar.compose": "編輯",
   "tabs_bar.federated_timeline": "聯盟",
   "tabs_bar.home": "家",
   "tabs_bar.local_timeline": "本地",
diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js
index f77061dfa..47e6d2330 100644
--- a/app/javascript/mastodon/reducers/accounts.js
+++ b/app/javascript/mastodon/reducers/accounts.js
@@ -102,7 +102,7 @@ const initialState = ImmutableMap();
 export default function accounts(state = initialState, action) {
   switch(action.type) {
   case STORE_HYDRATE:
-    return state.merge(action.state.get('accounts'));
+    return normalizeAccounts(state, Object.values(action.state.get('accounts').toJS()));
   case ACCOUNT_FETCH_SUCCESS:
   case NOTIFICATIONS_UPDATE:
     return normalizeAccount(state, action.account);
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 1358fb4aa..532f4b2a7 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -16,6 +16,8 @@ import {
   COMPOSE_SUGGESTIONS_CLEAR,
   COMPOSE_SUGGESTIONS_READY,
   COMPOSE_SUGGESTION_SELECT,
+  COMPOSE_SUGGESTION_TAGS_UPDATE,
+  COMPOSE_TAG_HISTORY_UPDATE,
   COMPOSE_SENSITIVITY_CHANGE,
   COMPOSE_SPOILERNESS_CHANGE,
   COMPOSE_SPOILER_TEXT_CHANGE,
@@ -54,6 +56,7 @@ const initialState = ImmutableMap({
   default_sensitive: false,
   resetFileKey: Math.floor((Math.random() * 0x10000)),
   idempotencyKey: null,
+  tagHistory: ImmutableList(),
 });
 
 function statusToTextMentions(state, status) {
@@ -87,7 +90,6 @@ function appendMedia(state, media) {
     map.update('media_attachments', list => list.push(media));
     map.set('is_uploading', false);
     map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
-    map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`);
     map.set('focusDate', new Date());
     map.set('idempotencyKey', uuid());
 
@@ -98,12 +100,10 @@ function appendMedia(state, media) {
 };
 
 function removeMedia(state, mediaId) {
-  const media    = state.get('media_attachments').find(item => item.get('id') === mediaId);
   const prevSize = state.get('media_attachments').size;
 
   return state.withMutations(map => {
     map.update('media_attachments', list => list.filterNot(item => item.get('id') === mediaId));
-    map.update('text', text => text.replace(media.get('text_url'), '').trim());
     map.set('idempotencyKey', uuid());
 
     if (prevSize === 1) {
@@ -122,6 +122,18 @@ const insertSuggestion = (state, position, token, completion) => {
   });
 };
 
+const updateSuggestionTags = (state, token) => {
+  const prefix = token.slice(1);
+
+  return state.merge({
+    suggestions: state.get('tagHistory')
+      .filter(tag => tag.startsWith(prefix))
+      .slice(0, 4)
+      .map(tag => '#' + tag),
+    suggestion_token: token,
+  });
+};
+
 const insertEmoji = (state, position, emojiData) => {
   const emoji = emojiData.native;
 
@@ -252,6 +264,10 @@ export default function compose(state = initialState, action) {
     return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token);
   case COMPOSE_SUGGESTION_SELECT:
     return insertSuggestion(state, action.position, action.token, action.completion);
+  case COMPOSE_SUGGESTION_TAGS_UPDATE:
+    return updateSuggestionTags(state, action.token);
+  case COMPOSE_TAG_HISTORY_UPDATE:
+    return state.set('tagHistory', fromJS(action.tags));
   case TIMELINE_DELETE:
     if (action.id === state.get('in_reply_to')) {
       return state.set('in_reply_to', null);
diff --git a/app/javascript/mastodon/reducers/dropdown_menu.js b/app/javascript/mastodon/reducers/dropdown_menu.js
new file mode 100644
index 000000000..5449884cc
--- /dev/null
+++ b/app/javascript/mastodon/reducers/dropdown_menu.js
@@ -0,0 +1,18 @@
+import Immutable from 'immutable';
+import {
+  DROPDOWN_MENU_OPEN,
+  DROPDOWN_MENU_CLOSE,
+} from '../actions/dropdown_menu';
+
+const initialState = Immutable.Map({ openId: null, placement: null });
+
+export default function dropdownMenu(state = initialState, action) {
+  switch (action.type) {
+  case DROPDOWN_MENU_OPEN:
+    return state.merge({ openId: action.id, placement: action.placement });
+  case DROPDOWN_MENU_CLOSE:
+    return state.get('openId') === action.id ? state.set('openId', null) : state;
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index a028e989c..b84b2d18a 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -1,4 +1,5 @@
 import { combineReducers } from 'redux-immutable';
+import dropdown_menu from './dropdown_menu';
 import timelines from './timelines';
 import meta from './meta';
 import alerts from './alerts';
@@ -26,6 +27,7 @@ import lists from './lists';
 import listEditor from './list_editor';
 
 const reducers = {
+  dropdown_menu,
   timelines,
   meta,
   alerts,
diff --git a/app/javascript/mastodon/settings.js b/app/javascript/mastodon/settings.js
index dbd969cb1..7643a508e 100644
--- a/app/javascript/mastodon/settings.js
+++ b/app/javascript/mastodon/settings.js
@@ -44,3 +44,4 @@ export default class Settings {
 }
 
 export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
+export const tagHistory = new Settings('mastodon_tag_history');
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index 9ce83aa9b..c484f074b 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -194,6 +194,28 @@ $small-breakpoint: 960px;
     }
   }
 
+  .closed-registrations-message {
+    margin-top: 20px;
+
+    &,
+    p {
+      text-align: center;
+      font-size: 12px;
+      line-height: 18px;
+      color: $ui-primary-color;
+      margin-bottom: 0;
+
+      a {
+        color: $ui-highlight-color;
+        text-decoration: underline;
+      }
+    }
+
+    p:last-child {
+      margin-bottom: 0;
+    }
+  }
+
   em {
     display: inline;
     margin: 0;
@@ -832,8 +854,13 @@ $small-breakpoint: 960px;
   }
 
   &__features {
+    & > p {
+      padding-right: 60px;
+    }
+
     .features-list {
-      margin: 40px 0 !important;
+      margin: 40px 0;
+      margin-top: 30px;
     }
 
     &__action {
@@ -842,17 +869,11 @@ $small-breakpoint: 960px;
   }
 
   .features-list {
-    margin-top: 20px;
-
     .features-list__row {
       display: flex;
       padding: 10px 0;
       justify-content: space-between;
 
-      &:first-child {
-        padding-top: 0;
-      }
-
       .visual {
         flex: 0 0 auto;
         display: flex;
@@ -878,6 +899,14 @@ $small-breakpoint: 960px;
         }
       }
     }
+
+    @media screen and (min-width: $small-breakpoint) {
+      display: grid;
+      grid-gap: 30px;
+      grid-template-columns: 1fr 1fr;
+      grid-auto-columns: 50%;
+      grid-auto-rows: max-content;
+    }
   }
 
   .extended-description {
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 0c343e1df..e6bd0c717 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -105,6 +105,16 @@
       margin-bottom: 30px;
     }
 
+    h4 {
+      text-transform: uppercase;
+      font-size: 13px;
+      font-weight: 500;
+      color: $ui-primary-color;
+      padding-bottom: 8px;
+      margin-bottom: 8px;
+      border-bottom: 1px solid lighten($ui-base-color, 8%);
+    }
+
     h6 {
       font-size: 16px;
       color: $ui-secondary-color;
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index d8364ef81..253523540 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -862,12 +862,27 @@
   border-bottom: 1px solid $ui-secondary-color;
   display: flex;
 
-  .status__content {
-    flex: 1 1 auto;
-    padding: 10px;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
+  .status-check-box__status {
+    margin: 10px 0 10px 10px;
+    flex: 1;
+
+    .media-gallery {
+      max-width: 250px;
+    }
+
+    .status__content {
+      padding: 0;
+      white-space: normal;
+    }
+
+    .video-player {
+      margin-top: 8px;
+      max-width: 250px;
+    }
+
+    .media-gallery__item-thumbnail {
+      cursor: default;
+    }
   }
 }
 
@@ -1418,36 +1433,29 @@
 
 .image-loader {
   position: relative;
+  width: 100%;
+  height: 100%;
 
   &.image-loader--loading {
+    display: flex;
+    align-content: center;
+
     .image-loader__preview-canvas {
       filter: blur(2px);
     }
   }
 
-  .image-loader__img {
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    max-width: 100%;
-    max-height: 100%;
-    background-image: none;
+  &.image-loader--amorphous .image-loader__preview-canvas {
+    display: none;
   }
+}
 
-  &.image-loader--amorphous {
-    position: static;
-
-    .image-loader__preview-canvas {
-      display: none;
-    }
-
-    .image-loader__img {
-      position: static;
-      width: auto;
-      height: auto;
-    }
-  }
+.zoomable-image {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-content: center;
 }
 
 .navigation-bar {
@@ -2784,33 +2792,26 @@ a.status-card {
   }
 }
 
-.modal-container__nav {
-  align-items: center;
-  background: rgba($base-overlay-background, 0.5);
-  box-sizing: border-box;
-  border: 0;
+.account--follows-info {
   color: $primary-text-color;
-  cursor: pointer;
-  display: flex;
-  font-size: 24px;
-  height: 100%;
-  padding: 30px 15px;
   position: absolute;
-  top: 0;
-}
-
-.modal-container__nav--left {
-  left: -61px;
-}
-
-.modal-container__nav--right {
-  right: -61px;
+  top: 10px;
+  left: 10px;
+  opacity: 0.7;
+  display: inline-block;
+  vertical-align: top;
+  background-color: rgba($base-overlay-background, 0.4);
+  text-transform: uppercase;
+  font-size: 11px;
+  font-weight: 500;
+  padding: 4px;
+  border-radius: 4px;
 }
 
-.account--follows-info {
+.account--muting-info {
   color: $primary-text-color;
   position: absolute;
-  top: 10px;
+  top: 40px;
   left: 10px;
   opacity: 0.7;
   display: inline-block;
@@ -3403,29 +3404,27 @@ a.status-card {
   z-index: 9999;
 }
 
+.video-modal {
+  max-width: 100vw;
+  max-height: 100vh;
+  position: relative;
+}
+
 .media-modal {
-  max-width: 80vw;
-  max-height: 80vh;
+  width: 100%;
+  height: 100%;
   position: relative;
 
-  .extended-video-player,
   img,
   canvas,
   video {
-    max-width: 80vw;
-    max-height: 80vh;
+    max-width: 100vw;
+    max-height: 100vh;
     width: auto;
     height: auto;
     margin: auto;
   }
 
-  .extended-video-player,
-  video {
-    display: flex;
-    width: 80vw;
-    height: 80vh;
-  }
-
   img,
   canvas {
     display: block;
@@ -3434,12 +3433,65 @@ a.status-card {
   }
 
   .react-swipeable-view-container {
-    max-width: 80vw;
+    width: 100vw;
+    height: 100%;
   }
 }
 
-.media-modal__content {
-  background: $base-overlay-background;
+.media-modal__closer {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+
+.media-modal__navigation {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  pointer-events: none;
+  transition: opacity 0.3s linear;
+  will-change: opacity;
+
+  * {
+    pointer-events: auto;
+  }
+
+  &.media-modal__navigation--hidden {
+    opacity: 0;
+
+    * {
+      pointer-events: none;
+    }
+  }
+}
+
+.media-modal__nav {
+  background: rgba($base-overlay-background, 0.5);
+  box-sizing: border-box;
+  border: 0;
+  color: $primary-text-color;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  font-size: 24px;
+  height: 20vmax;
+  margin: auto 0;
+  padding: 30px 15px;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+}
+
+.media-modal__nav--left {
+  left: 0;
+}
+
+.media-modal__nav--right {
+  right: 0;
 }
 
 .media-modal__pagination {
@@ -3447,7 +3499,8 @@ a.status-card {
   text-align: center;
   position: absolute;
   left: 0;
-  bottom: -40px;
+  bottom: 20px;
+  pointer-events: none;
 }
 
 .media-modal__page-dot {
@@ -3471,8 +3524,8 @@ a.status-card {
 
 .media-modal__close {
   position: absolute;
-  right: 4px;
-  top: 4px;
+  right: 8px;
+  top: 8px;
   z-index: 100;
 }
 
@@ -4169,45 +4222,59 @@ a.status-card {
   border-radius: 4px;
   margin-top: 14px;
   overflow: hidden;
-}
 
-.attachment-list__icon {
-  flex: 0 0 auto;
-  color: $ui-base-lighter-color;
-  padding: 8px 18px;
-  cursor: default;
-  border-right: 1px solid lighten($ui-base-color, 8%);
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  font-size: 26px;
+  &__icon {
+    flex: 0 0 auto;
+    color: $ui-base-lighter-color;
+    padding: 8px 18px;
+    cursor: default;
+    border-right: 1px solid lighten($ui-base-color, 8%);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    font-size: 26px;
 
-  .fa {
-    display: block;
+    .fa {
+      display: block;
+    }
   }
-}
-
-.attachment-list__list {
-  list-style: none;
-  padding: 4px 0;
-  padding-left: 8px;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
 
-  li {
-    display: block;
+  &__list {
+    list-style: none;
     padding: 4px 0;
+    padding-left: 8px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+
+    li {
+      display: block;
+      padding: 4px 0;
+    }
+
+    a {
+      text-decoration: none;
+      color: $ui-base-lighter-color;
+      font-weight: 500;
+
+      &:hover {
+        text-decoration: underline;
+      }
+    }
   }
 
-  a {
-    text-decoration: none;
-    color: $ui-base-lighter-color;
-    font-weight: 500;
+  &.compact {
+    border: 0;
+    margin-top: 4px;
 
-    &:hover {
-      text-decoration: underline;
+    .attachment-list__list {
+      padding: 0;
+      display: block;
+    }
+
+    .fa {
+      color: $ui-base-lighter-color;
     }
   }
 }
@@ -4613,7 +4680,7 @@ a.status-card {
     background-size: cover;
     background-position: center;
     position: absolute;
-    color: inherit;
+    color: $ui-primary-color;
     text-decoration: none;
     border-radius: 4px;
 
@@ -4621,6 +4688,7 @@ a.status-card {
     &:active,
     &:focus {
       outline: 0;
+      color: $ui-secondary-color;
 
       &::before {
         content: "";
@@ -4632,6 +4700,14 @@ a.status-card {
       }
     }
   }
+
+  &__icons {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    font-size: 24px;
+  }
 }
 
 .account__section-headline {
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 2e38cda4e..d74c5a4fd 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -22,16 +22,6 @@ code {
     margin-top: 4px;
   }
 
-  h4 {
-    text-transform: uppercase;
-    font-size: 13px;
-    font-weight: 500;
-    color: $ui-primary-color;
-    padding-bottom: 8px;
-    margin-bottom: 8px;
-    border-bottom: 1px solid lighten($ui-base-color, 8%);
-  }
-
   p.hint {
     margin-bottom: 15px;
     color: $ui-primary-color;
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 6f4a3b491..9b00f0f52 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -46,6 +46,10 @@ class ActivityPub::Activity
         ActivityPub::Activity::Reject
       when 'Flag'
         ActivityPub::Activity::Flag
+      when 'Add'
+        ActivityPub::Activity::Add
+      when 'Remove'
+        ActivityPub::Activity::Remove
       end
     end
   end
diff --git a/app/lib/activitypub/activity/add.rb b/app/lib/activitypub/activity/add.rb
new file mode 100644
index 000000000..ea94d2f98
--- /dev/null
+++ b/app/lib/activitypub/activity/add.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Add < ActivityPub::Activity
+  def perform
+    return unless @json['target'].present? && value_or_id(@json['target']) == @account.featured_collection_url
+
+    status = status_from_uri(object_uri)
+
+    return unless status.account_id == @account.id && !@account.pinned?(status)
+
+    StatusPin.create!(account: @account, status: status)
+  end
+end
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index a7afbb859..5a1c13d67 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -20,13 +20,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   private
 
   def process_status
-    media_attachments = process_attachments
+    status_params = process_status_params
 
     ApplicationRecord.transaction do
       @status = Status.create!(status_params)
 
       process_tags(@status)
-      attach_media(@status, media_attachments)
     end
 
     resolve_thread(@status)
@@ -40,7 +39,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     status
   end
 
-  def status_params
+  def process_status_params
     {
       uri: @object['id'],
       url: object_url || @object['id'],
@@ -54,6 +53,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
       visibility: visibility_from_audience,
       thread: replied_to_status,
       conversation: conversation_from_uri(@object['conversation']),
+      media_attachments: process_attachments.take(4),
     }
   end
 
@@ -108,7 +108,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   end
 
   def process_attachments
-    return if @object['attachment'].nil?
+    return [] if @object['attachment'].nil?
 
     media_attachments = []
 
@@ -132,13 +132,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     media_attachments
   end
 
-  def attach_media(status, media_attachments)
-    return if media_attachments.blank?
-
-    media = MediaAttachment.where(status_id: nil, id: media_attachments.take(4).map(&:id))
-    media.update(status_id: status.id)
-  end
-
   def resolve_thread(status)
     return unless status.reply? && status.thread.nil?
     ThreadResolveWorker.perform_async(status.id, in_reply_to_uri)
diff --git a/app/lib/activitypub/activity/remove.rb b/app/lib/activitypub/activity/remove.rb
new file mode 100644
index 000000000..62a1e3196
--- /dev/null
+++ b/app/lib/activitypub/activity/remove.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Remove < ActivityPub::Activity
+  def perform
+    return unless @json['target'].present? && value_or_id(@json['target']) == @account.featured_collection_url
+
+    status = status_from_uri(object_uri)
+
+    return unless status.account_id == @account.id
+
+    pin = StatusPin.find_by(account: @account, status: status)
+    pin&.destroy!
+  end
+end
diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb
index 8198ac580..f19b04ae6 100644
--- a/app/lib/activitypub/adapter.rb
+++ b/app/lib/activitypub/adapter.rb
@@ -18,6 +18,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
         'toot'                      => 'http://joinmastodon.org/ns#',
         'Emoji'                     => 'toot:Emoji',
         'focalPoint'                => { '@container' => '@list', '@id' => 'toot:focalPoint' },
+        'featured'                  => 'toot:featured',
       },
     ],
   }.freeze
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 8c0f8cebc..1df4ff8d4 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -19,6 +19,8 @@ class Formatter
 
     raw_content = status.text
 
+    return '' if raw_content.blank?
+
     unless status.local?
       html = reformat(raw_content)
       html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb
index 7cf2d90dc..aa46267dc 100644
--- a/app/lib/ostatus/activity/creation.rb
+++ b/app/lib/ostatus/activity/creation.rb
@@ -29,7 +29,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
     # Skip if the reblogged status is not public
     return if cached_reblog && !(cached_reblog.public_visibility? || cached_reblog.unlisted_visibility?)
 
-    media_attachments = save_media
+    media_attachments = save_media.take(4)
 
     ApplicationRecord.transaction do
       status = Status.create!(
@@ -44,12 +44,12 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
         language: content_language,
         visibility: visibility_scope,
         conversation: find_or_create_conversation,
-        thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil
+        thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil,
+        media_attachments: media_attachments
       )
 
       save_mentions(status)
       save_hashtags(status)
-      attach_media(status, media_attachments)
       save_emojis(status)
     end
 
@@ -159,13 +159,6 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
     media_attachments
   end
 
-  def attach_media(parent, media_attachments)
-    return if media_attachments.blank?
-
-    media = MediaAttachment.where(status_id: nil, id: media_attachments.take(4).map(&:id))
-    media.update(status_id: parent.id)
-  end
-
   def save_emojis(parent)
     do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
 
diff --git a/app/models/account.rb b/app/models/account.rb
index 1abd49b1e..61f81ab70 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -43,6 +43,7 @@
 #  protocol                :integer          default("ostatus"), not null
 #  memorial                :boolean          default(FALSE), not null
 #  moved_to_account_id     :integer
+#  featured_collection_url :string
 #
 
 class Account < ApplicationRecord
diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb
index 7712a29fd..9e34a9461 100644
--- a/app/models/concerns/account_avatar.rb
+++ b/app/models/concerns/account_avatar.rb
@@ -7,8 +7,8 @@ module AccountAvatar
 
   class_methods do
     def avatar_styles(file)
-      styles = { original: { geometry: '120x120#', file_geometry_parser: FastGeometryParser } }
-      styles[:static] = { geometry: '120x120#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
+      styles = { original: { geometry: '400x400#', file_geometry_parser: FastGeometryParser } }
+      styles[:static] = { geometry: '400x400#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
       styles
     end
 
diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb
index 87d93c1fd..50288e700 100644
--- a/app/models/concerns/omniauthable.rb
+++ b/app/models/concerns/omniauthable.rb
@@ -58,13 +58,14 @@ module Omniauthable
       email_is_verified = auth.info.verified || auth.info.verified_email || assume_verified
       email             = auth.info.verified_email || auth.info.email
       email             = email_is_verified && !User.exists?(email: auth.info.email) && email
+      display_name      = auth.info.full_name || [auth.info.first_name, auth.info.last_name].join(' ')
 
       {
         email: email ? email : "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com",
         password: Devise.friendly_token[0, 20],
         account_attributes: {
           username: ensure_unique_username(auth.uid),
-          display_name: [auth.info.first_name, auth.info.last_name].join(' '),
+          display_name: display_name,
         },
       }
     end
diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb
index 990035b34..020303a2f 100644
--- a/app/models/concerns/remotable.rb
+++ b/app/models/concerns/remotable.rb
@@ -28,7 +28,11 @@ module Remotable
           matches  = response.headers['content-disposition']&.match(/filename="([^"]*)"/)
           filename = matches.nil? ? parsed_url.path.split('/').last : matches[1]
           basename = SecureRandom.hex(8)
-          extname  = File.extname(filename)
+          extname = if filename.nil?
+                      ''
+                    else
+                      File.extname(filename)
+                    end
 
           send("#{attachment_name}=", StringIO.new(response.to_s))
           send("#{attachment_name}_file_name=", basename + extname)
diff --git a/app/models/status.rb b/app/models/status.rb
index 125fa1bb5..7e5ca09e4 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -58,7 +58,7 @@ class Status < ApplicationRecord
   has_one :stream_entry, as: :activity, inverse_of: :status
 
   validates :uri, uniqueness: true, presence: true, unless: :local?
-  validates :text, presence: true, unless: :reblog?
+  validates :text, presence: true, unless: -> { with_media? || reblog? }
   validates_with StatusLengthValidator
   validates :reblog, uniqueness: { scope: :account }, if: :reblog?
 
@@ -153,8 +153,12 @@ class Status < ApplicationRecord
     private_visibility? || direct_visibility?
   end
 
+  def with_media?
+    media_attachments.any?
+  end
+
   def non_sensitive_with_media?
-    !sensitive? && media_attachments.any?
+    !sensitive? && with_media?
   end
 
   def emojis
diff --git a/app/models/user.rb b/app/models/user.rb
index b3e5f9352..0346cf8ae 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -44,7 +44,7 @@ class User < ApplicationRecord
   ACTIVE_DURATION = 14.days
 
   devise :two_factor_authenticatable,
-         otp_secret_encryption_key: ENV.fetch('OTP_SECRET')
+         otp_secret_encryption_key: Rails.configuration.x.otp_secret
 
   devise :two_factor_backupable,
          otp_number_of_backup_codes: 10
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index 622bdde0c..afcd37771 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -4,7 +4,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
   include RoutingHelper
 
   attributes :id, :type, :following, :followers,
-             :inbox, :outbox,
+             :inbox, :outbox, :featured,
              :preferred_username, :name, :summary,
              :url, :manually_approves_followers
 
@@ -53,6 +53,10 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
     account_outbox_url(object)
   end
 
+  def featured
+    account_collection_url(object, :featured)
+  end
+
   def endpoints
     object
   end
diff --git a/app/serializers/activitypub/add_serializer.rb b/app/serializers/activitypub/add_serializer.rb
new file mode 100644
index 000000000..c0906e8d0
--- /dev/null
+++ b/app/serializers/activitypub/add_serializer.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class ActivityPub::AddSerializer < ActiveModel::Serializer
+  include RoutingHelper
+
+  attributes :type, :actor, :target
+  attribute :proper_object, key: :object
+
+  def type
+    'Add'
+  end
+
+  def actor
+    ActivityPub::TagManager.instance.uri_for(object.account)
+  end
+
+  def proper_object
+    ActivityPub::TagManager.instance.uri_for(object)
+  end
+
+  def target
+    account_collection_url(object.account, :featured)
+  end
+end
diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb
index d43af3f8e..1ae492945 100644
--- a/app/serializers/activitypub/collection_serializer.rb
+++ b/app/serializers/activitypub/collection_serializer.rb
@@ -2,7 +2,7 @@
 
 class ActivityPub::CollectionSerializer < ActiveModel::Serializer
   def self.serializer_for(model, options)
-    return ActivityPub::ActivitySerializer if model.class.name == 'Status'
+    return ActivityPub::NoteSerializer if model.class.name == 'Status'
     return ActivityPub::CollectionSerializer if model.class.name == 'ActivityPub::CollectionPresenter'
     super
   end
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index 24c39f3c9..d0e6290c1 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -90,6 +90,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
     include RoutingHelper
 
     attributes :type, :media_type, :url, :name
+    attribute :focal_point, if: :focal_point?
 
     def type
       'Document'
@@ -106,6 +107,14 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
     def url
       object.local? ? full_asset_url(object.file.url(:original, false)) : object.remote_url
     end
+
+    def focal_point?
+      object.file.meta.is_a?(Hash) && object.file.meta['focus'].is_a?(Hash)
+    end
+
+    def focal_point
+      [object.file.meta['focus']['x'], object.file.meta['focus']['y']]
+    end
   end
 
   class MentionSerializer < ActiveModel::Serializer
diff --git a/app/serializers/activitypub/outbox_serializer.rb b/app/serializers/activitypub/outbox_serializer.rb
new file mode 100644
index 000000000..48fbad0fd
--- /dev/null
+++ b/app/serializers/activitypub/outbox_serializer.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class ActivityPub::OutboxSerializer < ActivityPub::CollectionSerializer
+  def self.serializer_for(model, options)
+    return ActivityPub::ActivitySerializer if model.is_a?(Status)
+    super
+  end
+end
diff --git a/app/serializers/activitypub/remove_serializer.rb b/app/serializers/activitypub/remove_serializer.rb
new file mode 100644
index 000000000..c2a5ae1b3
--- /dev/null
+++ b/app/serializers/activitypub/remove_serializer.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class ActivityPub::RemoveSerializer < ActiveModel::Serializer
+  include RoutingHelper
+
+  attributes :type, :actor, :target
+  attribute :proper_object, key: :object
+
+  def type
+    'Remove'
+  end
+
+  def actor
+    ActivityPub::TagManager.instance.uri_for(object.account)
+  end
+
+  def proper_object
+    ActivityPub::TagManager.instance.uri_for(object)
+  end
+
+  def target
+    account_collection_url(object.account, :featured)
+  end
+end
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index 0168b18ea..cab05e60a 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -52,7 +52,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
   end
 
   def languages
-    [ENV.fetch('DEFAULT_LOCALE', I18n.default_locale)]
+    [I18n.default_locale]
   end
 
   private
diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb
new file mode 100644
index 000000000..40714e980
--- /dev/null
+++ b/app/services/activitypub/fetch_featured_collection_service.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+class ActivityPub::FetchFeaturedCollectionService < BaseService
+  include JsonLdHelper
+
+  def call(account)
+    @account = account
+    @json    = fetch_resource(@account.featured_collection_url, true)
+
+    return unless supported_context?
+    return if @account.suspended? || @account.local?
+
+    case @json['type']
+    when 'Collection', 'CollectionPage'
+      process_items @json['items']
+    when 'OrderedCollection', 'OrderedCollectionPage'
+      process_items @json['orderedItems']
+    end
+  end
+
+  private
+
+  def process_items(items)
+    status_ids = items.map { |item| value_or_id(item) }
+                      .reject { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }
+                      .map { |uri| ActivityPub::FetchRemoteStatusService.new.call(uri) }
+                      .compact
+                      .select { |status| status.account_id == @account.id }
+                      .map(&:id)
+
+    to_remove = []
+    to_add    = status_ids
+
+    StatusPin.where(account: @account).pluck(:status_id).each do |status_id|
+      if status_ids.include?(status_id)
+        to_add.delete(status_id)
+      else
+        to_remove << status_id
+      end
+    end
+
+    StatusPin.where(account: @account, status_id: to_remove).delete_all unless to_remove.empty?
+
+    to_add.each do |status_id|
+      StatusPin.create!(account: @account, status_id: status_id)
+    end
+  end
+
+  def supported_context?
+    super(@json)
+  end
+end
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index f43edafe7..68e9db766 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -27,6 +27,7 @@ class ActivityPub::ProcessAccountService < BaseService
 
     after_protocol_change! if protocol_changed?
     after_key_change! if key_changed?
+    check_featured_collection! if @account.featured_collection_url.present?
 
     @account
   rescue Oj::ParseError
@@ -57,14 +58,15 @@ class ActivityPub::ProcessAccountService < BaseService
   end
 
   def set_immediate_attributes!
-    @account.inbox_url        = @json['inbox'] || ''
-    @account.outbox_url       = @json['outbox'] || ''
-    @account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
-    @account.followers_url    = @json['followers'] || ''
-    @account.url              = url || @uri
-    @account.display_name     = @json['name'] || ''
-    @account.note             = @json['summary'] || ''
-    @account.locked           = @json['manuallyApprovesFollowers'] || false
+    @account.inbox_url               = @json['inbox'] || ''
+    @account.outbox_url              = @json['outbox'] || ''
+    @account.shared_inbox_url        = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
+    @account.followers_url           = @json['followers'] || ''
+    @account.featured_collection_url = @json['featured'] || ''
+    @account.url                     = url || @uri
+    @account.display_name            = @json['name'] || ''
+    @account.note                    = @json['summary'] || ''
+    @account.locked                  = @json['manuallyApprovesFollowers'] || false
   end
 
   def set_fetchable_attributes!
@@ -85,6 +87,10 @@ class ActivityPub::ProcessAccountService < BaseService
     RefollowWorker.perform_async(@account.id)
   end
 
+  def check_featured_collection!
+    ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id)
+  end
+
   def image_url(key)
     value = first_of_value(@json[key])
 
diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb
index eefdc0dbf..d082de40b 100644
--- a/app/services/block_domain_service.rb
+++ b/app/services/block_domain_service.rb
@@ -5,13 +5,14 @@ class BlockDomainService < BaseService
 
   def call(domain_block)
     @domain_block = domain_block
-    process_domain_block
+    process_domain_block!
   end
 
   private
 
-  def process_domain_block
+  def process_domain_block!
     clear_media! if domain_block.reject_media?
+
     if domain_block.silence?
       silence_accounts!
     elsif domain_block.suspend?
@@ -19,14 +20,26 @@ class BlockDomainService < BaseService
     end
   end
 
+  def invalidate_association_caches!
+    # Normally, associated models of a status are immutable (except for accounts)
+    # so they are aggressively cached. After updating the media attachments to no
+    # longer point to a local file, we need to clear the cache to make those
+    # changes appear in the API and UI
+    @affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") }
+  end
+
   def silence_accounts!
     blocked_domain_accounts.in_batches.update_all(silenced: true)
   end
 
   def clear_media!
-    clear_account_images
-    clear_account_attachments
-    clear_emojos
+    @affected_status_ids = []
+
+    clear_account_images!
+    clear_account_attachments!
+    clear_emojos!
+
+    invalidate_association_caches!
   end
 
   def suspend_accounts!
@@ -36,23 +49,25 @@ class BlockDomainService < BaseService
     end
   end
 
-  def clear_account_images
+  def clear_account_images!
     blocked_domain_accounts.find_each do |account|
-      account.avatar.destroy
-      account.header.destroy
+      account.avatar.destroy if account.avatar.exists?
+      account.header.destroy if account.header.exists?
       account.save
     end
   end
 
-  def clear_account_attachments
+  def clear_account_attachments!
     media_from_blocked_domain.find_each do |attachment|
-      attachment.file.destroy
+      @affected_status_ids << attachment.status_id if attachment.status_id.present?
+
+      attachment.file.destroy if attachment.file.exists?
       attachment.type = :unknown
       attachment.save
     end
   end
 
-  def clear_emojos
+  def clear_emojos!
     emojis_from_blocked_domains.destroy_all
   end
 
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 6b6a37676..74b4cba0c 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -21,17 +21,18 @@ class PostStatusService < BaseService
 
     media  = validate_media!(options[:media_ids])
     status = nil
+    text   = options.delete(:spoiler_text) if text.blank? && options[:spoiler_text].present?
+    text   = '.' if text.blank? && !media.empty?
 
     ApplicationRecord.transaction do
       status = account.statuses.create!(text: text,
+                                        media_attachments: media || [],
                                         thread: in_reply_to,
                                         sensitive: options[:sensitive],
                                         spoiler_text: options[:spoiler_text] || '',
                                         visibility: options[:visibility] || account.user&.setting_default_privacy,
                                         language: LanguageDetector.instance.detect(text, account),
                                         application: options[:application])
-
-      attach_media(status, media)
     end
 
     process_mentions_service.call(status)
@@ -67,11 +68,6 @@ class PostStatusService < BaseService
     media
   end
 
-  def attach_media(status, media)
-    return if media.nil?
-    media.update(status_id: status.id)
-  end
-
   def process_mentions_service
     ProcessMentionsService.new
   end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index fe9856686..00a8b3dd7 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -29,7 +29,9 @@ class SearchService < BaseService
   def perform_statuses_search!
     statuses = StatusesIndex.filter(term: { searchable_by: account.id })
                             .query(multi_match: { type: 'most_fields', query: query, operator: 'and', fields: %w(text text.stemmed) })
-                            .limit(limit).objects
+                            .limit(limit)
+                            .objects
+                            .compact
 
     statuses.reject { |status| StatusFilter.new(status, account).filtered? }
   end
diff --git a/app/views/about/_forms.html.haml b/app/views/about/_forms.html.haml
index 9916b6bf4..81f7173f7 100644
--- a/app/views/about/_forms.html.haml
+++ b/app/views/about/_forms.html.haml
@@ -1,12 +1,13 @@
 - if @instance_presenter.open_registrations
   = render 'registration'
 - else
-  - if @instance_presenter.closed_registrations_message.blank?
-    %p= t('about.closed_registrations')
-  - else
-    = @instance_presenter.closed_registrations_message.html_safe
+  = link_to t('auth.register_elsewhere'), 'https://joinmastodon.org', class: 'button button-primary'
 
-  = link_to t('auth.register'), 'https://joinmastodon.org', class: 'button button-primary'
+  .closed-registrations-message
+    - if @instance_presenter.closed_registrations_message.blank?
+      %p= t('about.closed_registrations')
+    - else
+      = @instance_presenter.closed_registrations_message.html_safe
 
 .separator-or
   %span= t('auth.or')
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 37bfde887..2f0b31a9f 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -2,6 +2,7 @@
   = site_hostname
 
 - content_for :header_tags do
+  %link{ rel: 'canonical', href: about_url }/
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
   = render partial: 'shared/og'
 
@@ -102,8 +103,10 @@
       - if Setting.timeline_preview
         .column-4.landing-page__information
           .landing-page__features
-            %h3= t 'about.what_is_mastodon'
-            %p= t 'about.about_mastodon_html'
+            .features-list
+              %div
+                %h3= t 'about.what_is_mastodon'
+                %p= t 'about.about_mastodon_html'
 
             = render 'features'
 
diff --git a/app/views/accounts/_og.html.haml b/app/views/accounts/_og.html.haml
index 1d16be590..26424a49c 100644
--- a/app/views/accounts/_og.html.haml
+++ b/app/views/accounts/_og.html.haml
@@ -1,7 +1,7 @@
 = opengraph 'og:url', url
 = opengraph 'og:site_name', site_title
 = opengraph 'og:title', [yield(:page_title).strip.presence, site_title].compact.join(' - ')
-= opengraph 'og:description', account.note
+= opengraph 'og:description', account_description(account)
 = opengraph 'og:image', full_asset_url(account.avatar.url(:original))
 = opengraph 'og:image:width', '120'
 = opengraph 'og:image:height', '120'
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 21c585dab..c62a573b0 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -1,7 +1,9 @@
 - content_for :page_title do
-  = display_name(@account)
+  = "#{display_name(@account)} (@#{@account.username})"
 
 - content_for :header_tags do
+  %meta{ name: 'description', content: account_description(@account) }/
+
   - if @account.user&.setting_noindex
     %meta{ name: 'robots', content: 'noindex' }/
 
@@ -9,6 +11,11 @@
   %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/
   %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
 
+  - if @older_url
+    %link{ rel: 'next', href: @older_url }/
+  - if @newer_url
+    %link{ rel: 'prev', href: @newer_url }/
+
   = opengraph 'og:type', 'profile'
   = render 'og', account: @account, url: short_account_url(@account, only_path: false)
 
@@ -42,6 +49,6 @@
   - if @newer_url || @older_url
     .pagination
       - if @older_url
-        = link_to safe_join([fa_icon('chevron-left'), t('pagination.older')], ' '), @older_url, class: 'older', rel: 'older'
+        = link_to safe_join([fa_icon('chevron-left'), t('pagination.older')], ' '), @older_url, class: 'older', rel: 'next'
       - if @newer_url
-        = link_to safe_join([t('pagination.newer'), fa_icon('chevron-right')], ' '), @newer_url, class: 'newer', rel: 'newer'
+        = link_to safe_join([t('pagination.newer'), fa_icon('chevron-right')], ' '), @newer_url, class: 'newer', rel: 'prev'
diff --git a/app/views/auth/passwords/edit.html.haml b/app/views/auth/passwords/edit.html.haml
index 12880c227..53d1769d6 100644
--- a/app/views/auth/passwords/edit.html.haml
+++ b/app/views/auth/passwords/edit.html.haml
@@ -4,7 +4,7 @@
 = simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f|
   = render 'shared/error_messages', object: resource
 
-  - if !use_seamless_external_login?? || resource.encrypted_password.present?
+  - if !use_seamless_external_login? || resource.encrypted_password.present?
     = f.input :reset_password_token, as: :hidden
 
     = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml
index 8424a8901..8586c0549 100644
--- a/app/views/auth/registrations/_sessions.html.haml
+++ b/app/views/auth/registrations/_sessions.html.haml
@@ -1,4 +1,4 @@
-%h6= t 'sessions.title'
+%h4= t 'sessions.title'
 %p.muted-hint= t 'sessions.explanation'
 
 .table-wrapper
diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml
index fac702b38..05fc7df31 100644
--- a/app/views/auth/registrations/edit.html.haml
+++ b/app/views/auth/registrations/edit.html.haml
@@ -1,6 +1,7 @@
 - content_for :page_title do
-  = t('auth.change_password')
+  = t('auth.security')
 
+%h4= t('auth.change_password')
 = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'auth_edit' }) do |f|
   = render 'shared/error_messages', object: resource
 
@@ -15,12 +16,9 @@
   - else
     %p.hint= t('users.seamless_external_login')
 
-%hr/
-
 = render 'sessions'
 
 - if open_deletion?
-  %hr/
 
-  %h6= t('auth.delete_account')
+  %h4= t('auth.delete_account')
   %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path)
diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml
index 738b31638..a24e4ea20 100644
--- a/app/views/follower_accounts/index.html.haml
+++ b/app/views/follower_accounts/index.html.haml
@@ -2,9 +2,7 @@
   = t('accounts.people_who_follow', name: display_name(@account))
 
 - content_for :header_tags do
-  - if @account.user&.setting_noindex
-    %meta{ name: 'robots', content: 'noindex' }/
-
+  %meta{ name: 'robots', content: 'noindex' }/
   = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false)
 
 = render 'accounts/header', account: @account
diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml
index 9637c689f..67f6cfede 100644
--- a/app/views/following_accounts/index.html.haml
+++ b/app/views/following_accounts/index.html.haml
@@ -2,9 +2,7 @@
   = t('accounts.people_followed_by', name: display_name(@account))
 
 - content_for :header_tags do
-  - if @account.user&.setting_noindex
-    %meta{ name: 'robots', content: 'noindex' }/
-
+  %meta{ name: 'robots', content: 'noindex' }/
   = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false)
 
 = render 'accounts/header', account: @account
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 322d7403e..475601fff 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -2,7 +2,7 @@
 %html{ lang: I18n.locale }
   %head
     %meta{ charset: 'utf-8' }/
-    %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }/   
+    %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }/
     %link{ rel: 'icon', href: favicon_path, type: 'image/x-icon' }/
     %link{ rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }/
     %link{ rel: 'mask-icon', href: '/mask-icon.svg', color: '#2B90D9' }/
@@ -11,11 +11,7 @@
     %meta{ name: 'theme-color', content: '#282c37' }/
     %meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/
 
-    %title<
-      - if content_for?(:page_title)
-        = yield(:page_title)
-        = ' - '
-      = title
+    %title= content_for?(:page_title) ? safe_join([yield(:page_title).chomp, ' - ', title]) : title
 
     = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
     - if @theme
diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml
index cf6671e67..a87c51952 100644
--- a/app/views/stream_entries/show.html.haml
+++ b/app/views/stream_entries/show.html.haml
@@ -1,5 +1,5 @@
 - content_for :page_title do
-  = t('statuses.title', name: display_name(@account), quote: truncate(@stream_entry.activity.spoiler_text.presence || @stream_entry.activity.text, length: 50, omission: '…'))
+  = t('statuses.title', name: display_name(@account), quote: truncate(@stream_entry.activity.spoiler_text.presence || @stream_entry.activity.text, length: 50, omission: '…', escape: false))
 
 - content_for :header_tags do
   - if @account.user&.setting_noindex
diff --git a/app/workers/activitypub/synchronize_featured_collection_worker.rb b/app/workers/activitypub/synchronize_featured_collection_worker.rb
new file mode 100644
index 000000000..dd676a3ee
--- /dev/null
+++ b/app/workers/activitypub/synchronize_featured_collection_worker.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ActivityPub::SynchronizeFeaturedCollectionWorker
+  include Sidekiq::Worker
+
+  sidekiq_options queue: 'pull'
+
+  def perform(account_id)
+    ActivityPub::FetchFeaturedCollectionService.new.call(Account.find(account_id))
+  rescue ActiveRecord::RecordNotFound
+    true
+  end
+end
diff --git a/boxfile.yml b/boxfile.yml
index d36a272e7..bb4149e70 100644
--- a/boxfile.yml
+++ b/boxfile.yml
@@ -1,7 +1,7 @@
 run.config:
   engine: ruby
   engine.config:
-    runtime: ruby-2.4
+    runtime: ruby-2.5
 
   extra_packages:
     # basic servers:
@@ -10,6 +10,7 @@ run.config:
 
     # for images:
     - ImageMagick
+    - jemalloc
 
     # for videos:
     - ffmpeg3
@@ -37,7 +38,7 @@ run.config:
     - yarn.lock
 
   extra_steps:
-    - envsubst < .env.nanobox > .env
+    - cp .env.nanobox .env
     - yarn
 
   fs_watch: true
@@ -47,7 +48,7 @@ deploy.config:
   extra_steps:
     - NODE_ENV=production bundle exec rake assets:precompile
   transform:
-    - "sed 's/LOCAL_HTTPS=.*/LOCAL_HTTPS=true/i' /app/.env.nanobox | envsubst > /app/.env.production"
+    - "envsubst < /app/.env.nanobox > /app/.env.production"
     - |-
         if [ -z "$LOCAL_DOMAIN" ]
         then
@@ -186,7 +187,7 @@ worker.cron_only:
 
 
 data.db:
-  image: nanobox/postgresql:9.5
+  image: nanobox/postgresql:9.6
 
   cron:
     - id: backup
@@ -196,11 +197,11 @@ data.db:
         gzip |
         curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/backup-${HOSTNAME}-$(date -u +%Y-%m-%d.%H-%M-%S).sql.gz --data-binary @- &&
         curl -k -s -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/ |
-        json_pp |
+        sed 's/,/\n/g' |
         grep ${HOSTNAME} |
         sort |
         head -n-${BACKUP_COUNT:-1} |
-        sed 's/.*: "\(.*\)".*/\1/' |
+        sed 's/.*: \?"\(.*\)".*/\1/' |
         while read file
         do
           curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/${file} -X DELETE
@@ -208,7 +209,7 @@ data.db:
 
 
 data.redis:
-  image: nanobox/redis:3.0
+  image: nanobox/redis:4.0
 
   cron:
     - id: backup
@@ -216,11 +217,11 @@ data.redis:
       command: |
         curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/backup-${HOSTNAME}-$(date -u +%Y-%m-%d.%H-%M-%S).rdb --data-binary @/data/var/db/redis/dump.rdb &&
         curl -k -s -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/ |
-        json_pp |
+        sed 's/,/\n/g' |
         grep ${HOSTNAME} |
         sort |
         head -n-${BACKUP_COUNT:-1} |
-        sed 's/.*: "\(.*\)".*/\1/' |
+        sed 's/.*: \?"\(.*\)".*/\1/' |
         while read file
         do
           curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/${file} -X DELETE
@@ -237,11 +238,11 @@ data.storage:
         tar cz -C /data/var/db/unfs/ . |
         curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/backup-${HOSTNAME}-$(date -u +%Y-%m-%d.%H-%M-%S).tgz --data-binary @- &&
         curl -k -s -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/ |
-        json_pp |
+        sed 's/,/\n/g' |
         grep ${HOSTNAME} |
         sort |
         head -n-${BACKUP_COUNT:-1} |
-        sed 's/.*: "\(.*\)".*/\1/' |
+        sed 's/.*: \?"\(.*\)".*/\1/' |
         while read file
         do
           curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/${file} -X DELETE
diff --git a/config/application.rb b/config/application.rb
index 88d0eccf2..f63746e34 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -32,7 +32,7 @@ module Mastodon
     # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
     # config.time_zone = 'Central Time (US & Canada)'
 
-    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
+    # All translations from config/locales/*.rb,yml are auto loaded.
     # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
     config.i18n.available_locales = [
       :en,
@@ -74,7 +74,12 @@ module Mastodon
       :'zh-TW',
     ]
 
-    config.i18n.default_locale = :en
+    config.i18n.default_locale = ENV['DEFAULT_LOCALE']&.to_sym
+    if config.i18n.available_locales.include?(config.i18n.default_locale)
+      config.i18n.fallbacks = [:en]
+    else
+      config.i18n.default_locale = :en
+    end
 
     # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
     # config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 2da407c32..285fea8b8 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -82,6 +82,8 @@ Rails.application.configure do
 
     Bullet.add_whitelist type: :n_plus_one_query, class_name: 'User', association: :account
   end
+
+  config.x.otp_secret = ENV.fetch('OTP_SECRET', '1fc2b87989afa6351912abeebe31ffc5c476ead9bf8b3d74cbc4a302c7b69a45b40b1bbef3506ddad73e942e15ed5ca4b402bf9a66423626051104f4b5f05109')
 end
 
 ActiveRecordQueryTrace.enabled = ENV.fetch('QUERY_TRACE_ENABLED') { false }
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 17915d6c6..6cd13d441 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -101,4 +101,6 @@ Rails.application.configure do
     'X-Clacks-Overhead' => 'GNU Natalie Nguyen'
 
   }
+
+  config.x.otp_secret = ENV.fetch('OTP_SECRET')
 end
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 20fe5f813..7d77a170e 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -44,6 +44,8 @@ Rails.application.configure do
   # Print deprecation notices to the stderr.
   config.active_support.deprecation = :stderr
 
+  config.x.otp_secret = '100c7faeef00caa29242f6b04156742bf76065771fd4117990c4282b8748ff3d99f8fdae97c982ab5bd2e6756a159121377cce4421f4a8ecd2d67bd7749a3fb4'
+
   # Generate random VAPID keys
   vapid_key = Webpush.generate_key
   config.x.vapid_private_key = vapid_key.private_key
@@ -51,6 +53,9 @@ Rails.application.configure do
 
   # Raises error for missing translations
   # config.action_view.raise_on_missing_translations = true
+
+  config.i18n.default_locale = :en
+  config.i18n.fallbacks = true
 end
 
 Paperclip::Attachment.default_options[:path] = "#{Rails.root}/spec/test_files/:class/:id_partition/:style.:extension"
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 92a73d82a..85fb81250 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -55,6 +55,8 @@ Devise.setup do |config|
     saml_options[:attribute_statements][:uid] = [ENV['SAML_ATTRIBUTES_STATEMENTS_UID']] if ENV['SAML_ATTRIBUTES_STATEMENTS_UID']
     saml_options[:attribute_statements][:email] = [ENV['SAML_ATTRIBUTES_STATEMENTS_EMAIL']] if ENV['SAML_ATTRIBUTES_STATEMENTS_EMAIL']
     saml_options[:attribute_statements][:full_name] = [ENV['SAML_ATTRIBUTES_STATEMENTS_FULL_NAME']] if ENV['SAML_ATTRIBUTES_STATEMENTS_FULL_NAME']
+    saml_options[:attribute_statements][:first_name] = [ENV['SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME']] if ENV['SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME']
+    saml_options[:attribute_statements][:last_name] = [ENV['SAML_ATTRIBUTES_STATEMENTS_LAST_NAME']] if ENV['SAML_ATTRIBUTES_STATEMENTS_LAST_NAME']
     saml_options[:attribute_statements][:verified] = [ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED']] if ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED']
     saml_options[:attribute_statements][:verified_email] = [ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL']] if ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL']
     saml_options[:uid_attribute] = ENV['SAML_UID_ATTRIBUTE'] if ENV['SAML_UID_ATTRIBUTE']
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index 6ec498efa..41d83e4ad 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -123,6 +123,7 @@ ar:
       emoji: إيموجي
       enable: تفعيل
       image_hint: ملف PNG إلى غاية حجم 50 ك.ب
+      shortcode_hint: على الأقل حرفين، و فقط رموز أبجدية عددية و أسطر سفلية
       title: الإيموجي الخاصة
       upload: رفع
     domain_blocks:
@@ -188,6 +189,8 @@ ar:
       site_title: إسم مثيل الخادم
       thumbnail:
         title: الصورة الرمزية المصغرة لمثيل الخادوم
+      timeline_preview:
+        desc_html: عرض الخيط العمومي على صفحة الإستقبال
       title: إعدادات الموقع
     statuses:
       back_to_account: العودة إلى صفحة الحساب
@@ -218,14 +221,15 @@ ar:
     regenerate_token: إعادة توليد رمز النفاذ
     your_token: رمز نفاذك
   auth:
-    change_password: الهوية
     confirm_email: تأكيد عنوان البريد الإلكتروني
     delete_account: حذف حساب
+    delete_account_html: إن كنت ترغب في حذف حسابك يُمكنك <a href="%{path}">المواصلة هنا</a>. سوف يُطلَبُ منك التأكيد قبل الحذف.
     didnt_get_confirmation: لم تتلق تعليمات التأكيد ؟
     forgot_password: نسيت كلمة المرور ؟
     login: تسجيل الدخول
     logout: خروج
     migrate_account: الإنتقال إلى حساب آخر
+    migrate_account_html: إن كنت ترغب في تحويل هذا الحساب نحو حساب آخَر، يُمكِنُك <a href="%{path}">إعداده هنا</a>.
     or_log_in_with: أو قم بتسجيل الدخول بواسطة
     providers:
       cas: CAS
@@ -233,6 +237,7 @@ ar:
     register: إنشاء حساب
     resend_confirmation: إعادة إرسال تعليمات التأكيد
     reset_password: إعادة تعيين كلمة المرور
+    security: الهوية
     set_new_password: تعيين كلمة مرور جديدة
   authorize_follow:
     error: يا للأسف، وقع هناك خطأ إثر عملية البحث عن الحساب عن بعد
@@ -414,6 +419,7 @@ ar:
       weibo: وايبو
     current_session: الجلسة الحالية
     description: "%{browser} على %{platform}"
+    explanation: ها هي قائمة مُتصفِّحات الويب  التي تستخدِم حاليًا حساب ماستدون الخاص بك.
     ip: عنوان الإيبي
     platforms:
       adobe_air: أدوبي إيير
@@ -455,7 +461,8 @@ ar:
       private_long: إعرضه لمتتبعيك فقط
       public: للعامة
       public_long: يمكن للجميع رؤيته
-      unlisted: Public, but do not display on the public timeline
+      unlisted: غير مُدرَج
+      unlisted_long: يُمكن لأيٍ كان رُؤيتَه و لكن لن يُعرَض على الخيوط العامة
   stream_entries:
     click_to_show: إضغط للعرض
     reblogged: رقى
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index 56a904895..cb3ed2244 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -30,7 +30,6 @@ bg:
   applications:
     invalid_url: Предоставеният URL е невалиден
   auth:
-    change_password: Идентификационни данни
     didnt_get_confirmation: Не получих инструкции за потвърждение
     forgot_password: Забравих си паролата
     login: Влизане
@@ -38,6 +37,7 @@ bg:
     register: Регистрация
     resend_confirmation: Изпрати отново инструкции за потвърждение
     reset_password: Подновяване на паролата
+    security: Идентификационни данни
     set_new_password: Задай нова парола
   authorize_follow:
     error: Възникна грешка в откриването на потребителя
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 902536ce4..ca353bba7 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -273,6 +273,9 @@ ca:
       contact_information:
         email: Introdueix una adreça de correu electrònic píblica
         username: Nom d'usuari del contacte
+      hero:
+        desc_html: Es mostra en pàgina frontal. Recomanat 600x100px al menys. Si no es configura es mostrarà el de la instància
+        title: Imatge d’heroi
       peers_api_enabled:
         desc_html: Els noms de domini que ha trobat aquesta instància al fediverse
         title: Publica la llista d'instàncies descobertes
@@ -356,7 +359,6 @@ ca:
     your_token: El teu identificador d'accés
   auth:
     agreement_html: En inscriure't, acceptes seguir <a href="%{rules_path}">els nostres termes del servei</a> i <a href="%{terms_path}">la nostra política de privadesa</a>.
-    change_password: Seguretat
     confirm_email: Confirmar correu electrònic
     delete_account: Suprimeix el compte
     delete_account_html: Si vols suprimir el compte pots <a href="%{path}">fer-ho aquí</a>. Se't demanarà confirmació.
@@ -367,6 +369,7 @@ ca:
     logout: Tanca sessió
     migrate_account: Mou a un compte diferent
     migrate_account_html: Si vols redirigir aquest compte a un altre diferent, el pots  <a href="%{path}">configurar aquí</a>.
+    or: o
     or_log_in_with: O inicia sessió amb
     providers:
       cas: CAS
@@ -374,6 +377,7 @@ ca:
     register: Registre
     resend_confirmation: Torna a enviar el correu de confirmació
     reset_password: Restableix la contrasenya
+    security: Seguretat
     set_new_password: Estableix una contrasenya nova
   authorize_follow:
     error: Malauradament, ha ocorregut un error cercant el compte remot
@@ -420,6 +424,13 @@ ca:
       title: Aquesta pàgina no es correcta
     noscript_html: Per a utilitzar Mastodon, activa el JavaScript. També pots provar una de les <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md"> aplicacions natives</a> de Mastodon per a la vostra plataforma.
   exports:
+    archive_takeout:
+      date: Data
+      download: Descarrega l’arxiu
+      hint_html: Pots sol·licitar un arxiu dels teus <strong>toots i els fitxers multimèdia pujats</strong>. Les dades exportades tindran el format ActivityPub, llegible per qualsevol programari compatible.
+      in_progress: Compilant el teu arxiu...
+      request: Sol·licita el teu arxiu
+      size: Tamany
     blocks: Persones que has blocat
     csv: CSV
     follows: Persones que segueixes
@@ -535,7 +546,9 @@ ca:
           trillion: T
           unit: " "
   pagination:
+    newer: Més recent
     next: Endavant
+    older: Més vell
     prev: Enrere
     truncate: "&hellip;"
   preferences:
@@ -730,6 +743,10 @@ ca:
     setup: Establir
     wrong_code: El codi introduït no és vàlid! És correcta l'hora del servidor i del dispositiu?
   user_mailer:
+    backup_ready:
+      explanation: Has sol·licitat una copia completa del teu compte Mastodon. Ara ja està a punt per descàrrega!
+      subject: El teu arxiu està preparat per a descàrrega
+      title: Recollida del arxiu
     welcome:
       edit_profile_action: Configurar perfil
       edit_profile_step: Pots personalitzar el teu perfil penjant un avatar, un encapçalament, canviant el teu nom de visualització i molt més. Si prefereixes revisar els seguidors nous abans de que et puguin seguir, pots blocar el teu compte.
@@ -751,4 +768,5 @@ ca:
   users:
     invalid_email: L'adreça de correu no és correcta
     invalid_otp_token: El codi de dos factors no és correcte
+    seamless_external_login: Has iniciat sessió via un servei extern per tant els ajustos de contrasenya i correu electrònic no estan disponibles.
     signed_in_as: 'Sessió iniciada com a:'
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 8f17413e1..e55ad151a 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -353,7 +353,6 @@ de:
     your_token: Dein Zugangs-Token
   auth:
     agreement_html: Indem du dich registrierst, erklärst du dich mit den Regeln, die <a href="%{rules_path}">auf dieser Instanz gelten</a> und der <a href="%{terms_path}">Datenschutzerklärung</a> einverstanden.
-    change_password: Sicherheit
     delete_account: Konto löschen
     delete_account_html: Falls du dein Konto löschen willst, kannst du <a href="%{path}">hier damit fortfahren</a>. Du wirst um Bestätigung gebeten werden.
     didnt_get_confirmation: Keine Bestätigungs-Mail erhalten?
@@ -366,6 +365,7 @@ de:
     register: Registrieren
     resend_confirmation: Bestätigungs-Mail erneut versenden
     reset_password: Passwort zurücksetzen
+    security: Sicherheit
     set_new_password: Neues Passwort setzen
   authorize_follow:
     error: Das Profil konnte nicht geladen werden
diff --git a/config/locales/devise.ar.yml b/config/locales/devise.ar.yml
index 564231a1d..4e302d3ea 100644
--- a/config/locales/devise.ar.yml
+++ b/config/locales/devise.ar.yml
@@ -49,6 +49,7 @@ ar:
       failure: تعذرت المصادقة من %{kind} بسبب "%{reason}".
       success: تمت المصادقة بنجاح عبر حساب %{kind}.
     passwords:
+      no_token: ليس بإمكانك النفاذ إلى هذه الصفحة  إن لم تقم بالنقر على الرابط المتواجد في الرسالة الإلكترونية. الرجاء التحقق مِن أنك قمت بإدخال عنوان الرابط كاملا كما هو مذكور في رسالة إعادة تعيين الكلمة السرية.
       send_instructions: إن كان عنوان بريدك الإلكتروني ضمن قاعدة بياناتنا، فسوف تتلقّى في غضون دقائق رابطا يُمكّنُك مِن استعادة كلمتك السرية على عنوان علبة البريد الإلكتروني الخاصة بك.إن لم تجد هذه الرسالة، يرجى تفقد مجلّد البريد المزعج.
       send_paranoid_instructions: إن كان عنوان بريدك الإلكتروني ضمن قاعدة بياناتنا، فسوف تتلقّى في غضون دقائق رابطا يُمكّنُك مِن استعادة كلمتك السرية على عنوان علبة البريد الإلكتروني الخاصة بك.إن لم تجد هذه الرسالة، يرجى تفقد مجلّد البريد المزعج.
       updated: تم تغيير كلمة المرور بنجاح. أنت مسجل الآن.
@@ -72,6 +73,10 @@ ar:
   errors:
     messages:
       already_confirmed: قمت بتأكيده من قبل، يرجى إعادة محاولة تسجيل الدخول
+      confirmation_period_expired: يجب التأكد منه قبل انقضاء مدة %{period}، يرجى إعادة طلب جديد
       expired: إنتهت مدة صلاحيته، الرجاء طلب واحد جديد
       not_found: لا يوجد
       not_locked: ليس مقفلاً
+      not_saved:
+        one: 'خطأ واحد منَعَ %{resource} مِن القيام بالإحتفاظ :'
+        other: "%{count} أخطاء منعت %{resource} مِن القيام بالإحتفاظ :"
diff --git a/config/locales/devise.sk.yml b/config/locales/devise.sk.yml
index f71f8c62c..2ce328d22 100644
--- a/config/locales/devise.sk.yml
+++ b/config/locales/devise.sk.yml
@@ -46,8 +46,8 @@ sk:
       unlock_instructions:
         subject: 'Mastodon: Inštrukcie pre odomknutie účtu'
     omniauth_callbacks:
-      failure: Nebolo možné vás autentifikovať z %{kind} z dôvodu "%{reason}".
-      success: Úspešne autentifikovaný z účtu %{kind}.
+      failure: Nebolo možné ťa overiť z dôvodu,%{kind} že "%{reason}".
+      success: Úspešné overenie z účtu %{kind}.
     passwords:
       no_token: Túto stránku nemôžete navštíviť pokiaľ neprichádzate z emailu s inštrukciami na obnovu hesla. Pokiaľ prichádzate z tohto emailu, prosím uistite sa že ste použili celú URL z emailu.
       send_instructions: Ak zadaný email existuje v našej databázi, tak o niekoľko minút obdržíte email s inštrukciami ako nastaviť nové heslo.
@@ -57,8 +57,8 @@ sk:
     registrations:
       destroyed: Dovidenia! Váš účet bol úspešne zrušený. Dúfame ale, že sa tu opäť niekedy zastavíte.
       signed_up: Vitajte! Vaša registrácia bola úspešná.
-      signed_up_but_inactive: Registrácia bola úspešná. Avšak, účet ešte nebol aktivovaný, takže vás nemôžeme prihlásiť.
-      signed_up_but_locked: Prihlasovanie úspešné. Avšak, účet je zablokovaný, takže vás nemôžeme prihlásiť.
+      signed_up_but_inactive: Registrácia bola úspešná. Avšak, účet ešte nebol aktivovaný, takže ťa nemôžeme prihlásiť.
+      signed_up_but_locked: Prihlasovanie úspešné. Avšak tvoj účet je zamknutý, takže ťa nieje možné prihlásiť.
       signed_up_but_unconfirmed: Správa s odkazom potvrdzujúcim registráciu bola poslaná na váš email. Pre aktváciu účtu, kliknite na daný odkaz.
       update_needs_confirmation: Účet bol úspešne zmenený ale ešte potrebujeme overiť vašu novú emailovú adresu. Pre overenie prosím kliknite na link v správe ktorú sme vám poslali na email.
       updated: Váš účet bol úspešne aktualizovaný.
diff --git a/config/locales/doorkeeper.sk.yml b/config/locales/doorkeeper.sk.yml
index a54faebf7..b8fd281f7 100644
--- a/config/locales/doorkeeper.sk.yml
+++ b/config/locales/doorkeeper.sk.yml
@@ -91,8 +91,8 @@ sk:
           unknown: Prístupový token je neplatný
         resource_owner_authenticator_not_configured: Resource Owner zlyhal pretože Doorkeeper.configure.resource_owner_authenticator nebol nakonfigurovaný.
         server_error: Nastala neočakávaná chyba na autorizačnom serveri ktorá zabránila vykonať požiadavku.
-        temporarily_unavailable: Autorizačný server Vás teraz nemôže obslúžiť pretože prebieha údržba alebo je dočasne preťažený.
-        unauthorized_client: Klient nie je autorizovaný vykonať túto požiadavku touto metódou.
+        temporarily_unavailable: Autorizačný server ťa teraz nemôže obslúžiť, pretože prebieha údržba alebo je dočasne preťažený.
+        unauthorized_client: Klient nie je autorizovaný vykonať danú požiadavku takouto metódou.
         unsupported_grant_type: Tento typ oprávnenia nie je podporovaný autorizačným serverom.
         unsupported_response_type: Autorizačný server nepodporuje typ tejto odpovede.
     flash:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 15e3400f1..e5e3ddf62 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -359,7 +359,7 @@ en:
     your_token: Your access token
   auth:
     agreement_html: By signing up you agree to follow <a href="%{rules_path}">the rules of the instance</a> and <a href="%{terms_path}">our terms of service</a>.
-    change_password: Security
+    change_password: Password
     confirm_email: Confirm email
     delete_account: Delete account
     delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
@@ -376,8 +376,10 @@ en:
       cas: CAS
       saml: SAML
     register: Sign up
+    register_elsewhere: Sign up on another server
     resend_confirmation: Resend confirmation instructions
     reset_password: Reset password
+    security: Security
     set_new_password: Set new password
   authorize_follow:
     error: Unfortunately, there was an error looking up the remote account
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index 0a8756b7d..6daa1caed 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -273,6 +273,8 @@ eo:
       contact_information:
         email: Publika retadreso
         username: Kontakta uzantnomo
+      hero:
+        desc_html: Montrata en la ĉefpaĝo. Almenaŭ 600x100px rekomendita. Kiam ne agordita, la bildeto de la nodo estos uzata
       peers_api_enabled:
         desc_html: Nomoj de domajnoj, kiujn ĉi tiu nodo renkontis en la fediverse
         title: Publikigi liston de malkovritaj nodoj
@@ -356,7 +358,6 @@ eo:
     your_token: Via alira ĵetono
   auth:
     agreement_html: Per registriĝo, vi konsentas kun <a href="%{rules_path}">la reguloj de la nodo</a> kaj <a href="%{terms_path}">niaj uzkondiĉoj</a>.
-    change_password: Sekureco
     confirm_email: Konfirmi retadreson
     delete_account: Forigi konton
     delete_account_html: Se vi deziras forigi vian konton, vi povas <a href="%{path}">fari tion ĉi tie</a>. Vi bezonos konfirmi vian peton.
@@ -374,6 +375,7 @@ eo:
     register: Registriĝi
     resend_confirmation: Resendi la instrukciojn por konfirmi
     reset_password: Ŝanĝi pasvorton
+    security: Sekureco
     set_new_password: Elekti novan pasvorton
   authorize_follow:
     error: Bedaŭrinde, estis eraro en la serĉado de la fora konto
@@ -422,6 +424,13 @@ eo:
       Por uzi la retan aplikaĵon de Mastodon, bonvolu ebligi JavaScript. Alimaniere, provu unu el la
       <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">operaciumaj aplikaĵoj</a> por Mastodon por via platformo.
   exports:
+    archive_takeout:
+      date: Dato
+      download: Elŝuti vian arkivon
+      hint_html: Vi povas peti arkivon de viaj <strong>mesaĝoj kaj alŝutitaj aŭdovidaĵoj</strong>. La eksportitaj datumoj estos en la formato ActivityPub, legebla de ajna konformema programo.
+      in_progress: Kunmetado de via arkivo…
+      request: Peti vian arkivon
+      size: Grandeco
     blocks: Vi blokas
     csv: CSV
     follows: Vi sekvas
@@ -537,7 +546,9 @@ eo:
           trillion: Dn
           unit: " "
   pagination:
+    newer: Pli nova
     next: Sekva
+    older: Malpli nova
     prev: Antaŭa
     truncate: "&hellip;"
   preferences:
@@ -734,6 +745,9 @@ eo:
     setup: Agordi
     wrong_code: La enmetita kodo estis nevalida! Ĉu la servila tempo kaj la aparata tempo ĝustas?
   user_mailer:
+    backup_ready:
+      explanation: Vi petis kompletan arkivon de via Mastodon-konto. Ĝi nun pretas por elŝutado!
+      subject: Via arkivo estas preta por elŝutado
     welcome:
       edit_profile_action: Agordi profilon
       edit_profile_step: Vi povas proprigi vian profilon per alŝuto de profilbildo, fonbildo, ŝanĝo de via afiŝita nomo kaj pli. Se vi ŝatus kontroli novajn sekvantojn antaŭ ol ili rajtas sekvi vin, vi povas ŝlosi vian konton.
@@ -755,4 +769,5 @@ eo:
   users:
     invalid_email: La retadreso estas nevalida
     invalid_otp_token: Nevalida kodo de dufaktora aŭtentigo
+    seamless_external_login: Vi estas ensalutinta per ekstera servo, do pasvortaj kaj retadresaj agordoj ne estas disponeblaj.
     signed_in_as: 'Ensalutinta kiel:'
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 102f9415e..490edcfee 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -353,7 +353,6 @@ es:
     your_token: Tu token de acceso
   auth:
     agreement_html: Al registrarte, acepta seguir <a href="%{rules_path}">las reglas de la instancia</a> y <a href="%{terms_path}">nuestros términos de servicio</a>.
-    change_password: Cambiar contraseña
     delete_account: Borrar cuenta
     delete_account_html: Si desea eliminar su cuenta, puede <a href="%{path}">proceder aquí</a>. Será pedido de una confirmación.
     didnt_get_confirmation: "¿No recibió el correo de confirmación?"
@@ -366,6 +365,7 @@ es:
     register: Registrarse
     resend_confirmation: Volver a enviar el correo de confirmación
     reset_password: Restablecer contraseña
+    security: Cambiar contraseña
     set_new_password: Establecer nueva contraseña
   authorize_follow:
     error: Desafortunadamente, ha ocurrido un error buscando la cuenta remota
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index 395d226bd..86756c01b 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -344,7 +344,6 @@ fa:
     your_token: کد دسترسی شما
   auth:
     agreement_html: پیش از عضو شدن باید <a href="%{rules_path}">قوانین این سرور</a> و <a href="%{terms_path}">شرایط استفادهٔ</a> ما را بپذیرید.
-    change_password: امنیت
     delete_account: پاک‌کردن حساب
     delete_account_html: اگر می‌خواهید حساب خود را پاک کنید، از <a href="%{path}">این‌جا</a> پیش بروید. از شما درخواست تأیید خواهد شد.
     didnt_get_confirmation: راهنمایی برای تأیید را دریافت نکردید؟
@@ -357,6 +356,7 @@ fa:
     register: عضو شوید
     resend_confirmation: راهنمایی برای تأیید را دوباره بفرست
     reset_password: بازنشانی رمز
+    security: امنیت
     set_new_password: تعیین رمز تازه
   authorize_follow:
     error: متأسفانه حین یافتن آن حساب خطایی رخ داد
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index e9c7273ce..71e019e54 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -67,7 +67,6 @@ fi:
   applications:
     invalid_url: Annettu URL on väärä
   auth:
-    change_password: Tunnukset
     didnt_get_confirmation: Etkö saanut varmennusohjeita?
     forgot_password: Unohditko salasanasi?
     login: Kirjaudu sisään
@@ -75,6 +74,7 @@ fi:
     register: Rekisteröidy
     resend_confirmation: Lähetä varmennusohjeet uudestaan
     reset_password: Palauta salasana
+    security: Tunnukset
     set_new_password: Aseta uusi salasana
   authorize_follow:
     error: Valitettavasti tapahtui virhe etätilin haussa.
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index cc330967d..2753b76dd 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -38,7 +38,7 @@ fr:
     followers: Abonné⋅e⋅s
     following: Abonnements
     media: Médias
-    moved_html: "%{name} a déménagé vers %{new_profile_link} :"
+    moved_html: "%{name} a changé de compte pour %{new_profile_link} :"
     nothing_here: Rien à voir ici !
     people_followed_by: Personnes suivies par %{name}
     people_who_follow: Personnes qui suivent %{name}
@@ -273,6 +273,9 @@ fr:
       contact_information:
         email: Entrez une adresse courriel publique
         username: Entrez un nom d’utilisateur⋅ice
+      hero:
+        desc_html: Affichée sur la page d'accueil. Au moins 600x100px recommandé. Lorsqu'elle n'est pas définie, se rabat sur la vignette de l'instance
+        title: Image d'en-tête
       peers_api_enabled:
         desc_html: Noms des domaines que cette instance a découvert dans le fediverse
         title: Publier la liste des instances découvertes
@@ -289,6 +292,9 @@ fr:
         open:
           desc_html: Autoriser tout le monde à créer un compte
           title: Ouvrir les inscriptions
+      show_known_fediverse_at_about_page:
+        desc_html: Lorsque l'option est activée, les pouets provenant de toutes les instances connues sont affichés dans la prévisualisation. Si non, seuls les pouets locaux sont affichés.
+        title: Afficher le fediverse connu dans la prévisualisation du fil
       show_staff_badge:
         desc_html: Montrer un badge de responsable sur une page utilisateur
         title: Montrer un badge de responsable
@@ -353,7 +359,6 @@ fr:
     your_token: Votre jeton d’accès
   auth:
     agreement_html: En vous inscrivant, vous souscrivez <a href="%{rules_path}">aux règles de l’instance</a> et à <a href="%{terms_path}">nos conditions d’utilisation</a>.
-    change_password: Sécurité
     confirm_email: Confirmer mon adresse mail
     delete_account: Supprimer le compte
     delete_account_html: Si vous désirez supprimer votre compte, vous pouvez <a href="%{path}">cliquer ici</a>. Il vous sera demandé de confirmer cette action.
@@ -364,6 +369,7 @@ fr:
     logout: Se déconnecter
     migrate_account: Déplacer vers un compte différent
     migrate_account_html: Si vous voulez rediriger ce compte vers un autre, vous pouvez le <a href="%{path}">configurer ici</a>.
+    or: ou
     or_log_in_with: Ou authentifiez-vous avec
     providers:
       cas: CAS
@@ -371,6 +377,7 @@ fr:
     register: S’inscrire
     resend_confirmation: Envoyer à nouveau les consignes de confirmation
     reset_password: Réinitialiser le mot de passe
+    security: Sécurité
     set_new_password: Définir le nouveau mot de passe
   authorize_follow:
     error: Malheureusement, il y a eu une erreur en cherchant les détails du compte distant
@@ -417,6 +424,13 @@ fr:
       title: Cette page n’est pas correcte
     noscript_html: Pour utiliser Mastodon, veuillez activer JavaScript. Sinon, essayez l'une des <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">applications natives</a> pour Mastodon pour votre plate-forme.
   exports:
+    archive_takeout:
+      date: Date
+      download: Télécharger votre archive
+      hint_html: Vous pouvez demander une archive de vos  <strong>pouets et médias téléversés</strong>. Les données exportées seront au format ActivityPub, lisible par tout logiciel compatible.
+      in_progress: Élaboration de votre archive....
+      request: Demandez vos archives
+      size: Taille
     blocks: Vous bloquez
     csv: CSV
     follows: Vous suivez
@@ -449,7 +463,7 @@ fr:
       following: Liste d’utilisateur⋅ice⋅s suivi⋅e⋅s
       muting: Liste d’utilisateur⋅ice⋅s que vous masquez
     upload: Importer
-  in_memoriam_html: In Memoriam.
+  in_memoriam_html: En mémoire de.
   invites:
     delete: Désactiver
     expired: Expiré
@@ -532,7 +546,9 @@ fr:
           trillion: T
           unit: ''
   pagination:
+    newer: Plus récent
     next: Suivant
+    older: Plus ancien
     prev: Précédent
     truncate: "&hellip;"
   preferences:
@@ -634,7 +650,7 @@ fr:
       unlisted_long: Tout le monde peut voir vos statuts mais ils ne seront pas sur listés sur les fils publics
   stream_entries:
     click_to_show: Cliquer pour afficher
-    pinned: Statut épinglé
+    pinned: Pouet épinglé
     reblogged: partagé
     sensitive_content: Contenu sensible
   terms:
@@ -727,6 +743,10 @@ fr:
     setup: Installer
     wrong_code: Les codes entrés sont incorrects ! L’heure du serveur et celle de votre appareil sont-elles correctes ?
   user_mailer:
+    backup_ready:
+      explanation: Vous avez demandé une sauvegarde complète de votre compte Mastodon. Elle est maintenant prête à être téléchargée !
+      subject: Votre archive est prête à être téléchargée
+      title: Retrait de l'archive
     welcome:
       edit_profile_action: Configuration du profil
       edit_profile_step: Vous pouvez personnaliser votre profil en téléchargeant un avatar, une image d'en-tête, en changeant votre pseudo et plus encore. Si vous souhaitez examiner les nouveaux abonnés avant qu'ils ne soient autorisés à vous suivre, vous pouvez verrouiller votre compte.
@@ -748,4 +768,5 @@ fr:
   users:
     invalid_email: L’adresse courriel est invalide
     invalid_otp_token: Le code d’authentification à deux facteurs est invalide
+    seamless_external_login: Vous êtes connecté via un service externe, donc les paramètres concernant le mot de passe et le courriel ne sont pas disponibles.
     signed_in_as: 'Connecté·e en tant que :'
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index 3333a842f..ca02ea693 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -273,6 +273,9 @@ gl:
       contact_information:
         email: e-mail de traballo
         username: Nome de usuaria de contacto
+      hero:
+        desc_html: Mostrado na portada. Recoméndase 600x100px como mínimo. Si non se establece, mostrará a imaxe por omisión da instancia
+        title: Imáxe Heróe
       peers_api_enabled:
         desc_html: Nome de dominio que esta instancia atopou no fediverso
         title: Publicar lista de instancias descubertas
@@ -356,7 +359,6 @@ gl:
     your_token: O seu testemuño de acceso
   auth:
     agreement_html: Rexistrándose acorda seguir <a href="%{rules_path}">as normas da instancia</a> e <a href="%{terms_path}">os termos do servizo</a>.
-    change_password: Seguridade
     confirm_email: Confirmar correo-e
     delete_account: Eliminar conta
     delete_account_html: Se desexa eliminar a súa conta, pode <a href="%{path}">facelo aquí</a>. Pediráselle confirmación.
@@ -367,6 +369,7 @@ gl:
     logout: Desconectar
     migrate_account: Mover a unha conta diferente
     migrate_account_html: Si desexa redirixir esta conta hacia outra diferente, pode <a href="%{path}">configuralo aquí</a>.
+    or: ou
     or_log_in_with: ou conectar con
     providers:
       cas: CAS
@@ -374,6 +377,7 @@ gl:
     register: Rexistro
     resend_confirmation: Voltar a enviar intruccións de confirmación
     reset_password: Restablecer contrasinal
+    security: Seguridade
     set_new_password: Establecer novo contrasinal
   authorize_follow:
     error: Desgraciadamente, algo fallou ao buscar a conta remota
@@ -420,6 +424,13 @@ gl:
       title: Esta páxina non é correcta
     noscript_html: Para utilizar a aplicación web de Mastodon debe habilitar JavaScript. De xeito alternativo, intente unha das <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">apps nativas</a> para Mastodon da súa plataforma.
   exports:
+    archive_takeout:
+      date: Data
+      download: Descargue o seu ficheiro
+      hint_html: Pode solicitar un ficheiro cos <strong>seus toots ficheiros de medios</strong>. Os datos estarán en formato ActivityPub e son compatibles con calquer software que o cumpla.
+      in_progress: Xerando o seu ficheiro...
+      request: Solicite o ficheiro
+      size: Tamaño
     blocks: A bloquear
     csv: CSV
     follows: A seguir
@@ -535,7 +546,9 @@ gl:
           trillion: T
           unit: " "
   pagination:
+    newer: Máis novo
     next: Seguinte
+    older: Máis antigo
     prev: Previo
     truncate: "&hellip;"
   preferences:
@@ -732,6 +745,10 @@ gl:
     setup: Configurar
     wrong_code: O código introducido non é válido! Son correctas as horas no dispositivo e o servidor?
   user_mailer:
+    backup_ready:
+      explanation: Solicitou un respaldo completo da súa conta de Mastodon. Xa está listo para descargar!
+      subject: O seu ficheiro xa está listo para descargar
+      title: Leve o ficheiro
     welcome:
       edit_profile_action: Configurar perfil
       edit_profile_step: Vostede pode personalizar o seu perfil subindo un avatar, cabeceira, cambiar o seu nome público e aínda máis. Si restrinxe a súa conta pode revisar a conta das personas que solicitan seguilas antes de permitirlles o acceso aos seus toots.
@@ -753,4 +770,5 @@ gl:
   users:
     invalid_email: O enderezo de correo non é válido
     invalid_otp_token: Código de doble-factor non válido
+    seamless_external_login: Está conectado a través de un servizo externo, polo que os axustes de contrasinal e correo-e non están dispoñibles.
     signed_in_as: 'Rexistrada como:'
diff --git a/config/locales/he.yml b/config/locales/he.yml
index c83f4ba10..1a7c84d7c 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -232,7 +232,6 @@ he:
   applications:
     invalid_url: כתובת הקישורית אינה חוקית
   auth:
-    change_password: החלפת סיסמא
     didnt_get_confirmation: לא התקבלו הוראות אימות?
     forgot_password: הנשתכחה סיסמתך?
     login: כניסה
@@ -240,6 +239,7 @@ he:
     register: הרשמה
     resend_confirmation: שלח הוראות אימות בשנית
     reset_password: איפוס סיסמא
+    security: החלפת סיסמא
     set_new_password: שינוי סיסמא
   authorize_follow:
     error: למרבה הצער, היתה שגיאה בחיפוש החשבון המרוחק
diff --git a/config/locales/hr.yml b/config/locales/hr.yml
index a3c9aa436..2d2eddc08 100644
--- a/config/locales/hr.yml
+++ b/config/locales/hr.yml
@@ -30,7 +30,6 @@ hr:
   applications:
     invalid_url: Uneseni link nije valjan
   auth:
-    change_password: Vjerodajnica
     didnt_get_confirmation: Niste primili instrukcije za potvrđivanje?
     forgot_password: Zaboravljena lozinka?
     login: Prijavi se
@@ -38,6 +37,7 @@ hr:
     register: Registriraj se
     resend_confirmation: Ponovo pošalji instrukcije za potvrđivanje
     reset_password: Resetiraj lozinku
+    security: Vjerodajnica
     set_new_password: Postavi novu lozinku
   authorize_follow:
     error: Nažalost, došlo je do greške looking up the remote račun
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index de35044d4..6be82c1de 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -353,7 +353,6 @@ hu:
     your_token: Hozzáférési kulcsod
   auth:
     agreement_html: A feliratkozással elfogatod az <a href="%{rules_path}">instancia szabályzatát</a> és a <a href="%{terms_path}">felhasználási feltételeket</a>.
-    change_password: Biztonság
     delete_account: Felhasználói fiók törlése
     delete_account_html: Felhasználói fiókod törléséhez <a href="%{path}">kattints ide</a>. A rendszer újbóli megerősítést fog kérni.
     didnt_get_confirmation: Nem kaptad meg a megerősítési lépéseket?
@@ -366,6 +365,7 @@ hu:
     register: Regisztráció
     resend_confirmation: Megerősítési lépések újraküldése
     reset_password: Jelszó visszaállítása
+    security: Biztonság
     set_new_password: Új jelszó beállítása
   authorize_follow:
     error: Hiba történt a távoli felhasználó keresésekor
diff --git a/config/locales/id.yml b/config/locales/id.yml
index 6e4d60fd8..0ef1d5040 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -155,7 +155,6 @@ id:
   applications:
     invalid_url: URL tidak sesuai
   auth:
-    change_password: Identitas
     didnt_get_confirmation: Tidak menerima petunjuk konfirmasi?
     forgot_password: Lupa kata sandi?
     login: Masuk
@@ -163,6 +162,7 @@ id:
     register: Daftar
     resend_confirmation: Kirim ulang email konfirmasi
     reset_password: Reset kata sandi
+    security: Identitas
     set_new_password: Tentukan kata sandi baru
   authorize_follow:
     error: Sayangnya, ada error saat melihat akun remote
diff --git a/config/locales/io.yml b/config/locales/io.yml
index db8214768..29ab4516b 100644
--- a/config/locales/io.yml
+++ b/config/locales/io.yml
@@ -153,7 +153,6 @@ io:
   applications:
     invalid_url: La URL donita ne esas valida
   auth:
-    change_password: Chanjar pasvorto
     didnt_get_confirmation: Ka tu ne recevis la instrucioni por konfirmar?
     forgot_password: Pasvorto obliviita?
     login: Enirar
@@ -161,6 +160,7 @@ io:
     register: Membreskar
     resend_confirmation: Risendar la instrucioni por konfirmar
     reset_password: Chanjar la pasvorto
+    security: Chanjar pasvorto
     set_new_password: Selektar nova pasvorto
   authorize_follow:
     error: Regretinde, eventis eraro probante konsultar la fora konto
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 6ab57d2fc..7e5bfd20e 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -30,7 +30,6 @@ it:
   applications:
     invalid_url: L'URL fornito non è valido
   auth:
-    change_password: Credenziali
     didnt_get_confirmation: Non hai ricevuto le istruzioni di conferma?
     forgot_password: Hai dimenticato la tua password?
     login: Entra
@@ -38,6 +37,7 @@ it:
     register: Iscriviti
     resend_confirmation: Invia di nuovo le istruzioni di conferma
     reset_password: Resetta la password
+    security: Credenziali
     set_new_password: Imposta una nuova password
   authorize_follow:
     error: Sfortunatamente c'è stato un errore nel consultare l'account remoto
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index bc7dc3735..eeba6f7e9 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -274,7 +274,7 @@ ja:
         email: ビジネスメールアドレス
         username: 連絡先のユーザー名
       hero:
-        desc_html: フロントページに表示されます。サイズは600x100px以上推奨です。未設定の場合、インスタンスのサムネイルが使用されます。
+        desc_html: フロントページに表示されます。サイズは600x100px以上推奨です。未設定の場合、インスタンスのサムネイルが使用されます
         title: ヒーローイメージ
       peers_api_enabled:
         desc_html: 連合内でこのインスタンスが遭遇したドメインの名前
@@ -359,7 +359,7 @@ ja:
     your_token: アクセストークン
   auth:
     agreement_html: 登録すると <a href="%{rules_path}">インスタンスのルール</a> と <a href="%{terms_path}">利用規約</a> に従うことに同意したことになります。
-    change_password: セキュリティ
+    confirm_email: メールアドレスの確認
     delete_account: アカウントの削除
     delete_account_html: アカウントを削除したい場合、<a href="%{path}">こちら</a> から手続きが行えます。削除する前に、確認画面があります。
     didnt_get_confirmation: 確認メールを受信できませんか?
@@ -369,10 +369,15 @@ ja:
     logout: ログアウト
     migrate_account: 別のアカウントに引っ越す
     migrate_account_html: 引っ越し先を明記したい場合は<a href="%{path}">こちら</a>で設定できます。
+    or: または
     or_log_in_with: または次のサービスでログイン
+    providers:
+      cas: CAS
+      saml: SAML
     register: 登録する
     resend_confirmation: 確認メールを再送する
     reset_password: パスワードを再発行
+    security: セキュリティ
     set_new_password: 新しいパスワード
   authorize_follow:
     error: 残念ながら、リモートアカウント情報の取得中にエラーが発生しました
@@ -550,7 +555,9 @@ ja:
           trillion: T
           unit: ''
   pagination:
+    newer: 新しいトゥート
     next: 次
+    older: 以前のトゥート
     prev: 前
     truncate: "&hellip;"
   preferences:
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index bb6e9a88e..249cb7bf1 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -275,6 +275,9 @@ ko:
       contact_information:
         email: 공개할 메일 주소를 입력
         username: 아이디를 입력
+      hero:
+        desc_html: 프론트페이지에 표시 됩니다. 최소 600x100픽셀을 권장합니다. 만약 설정되지 않았다면, 인스턴스의 썸네일이 사용 됩니다
+        title: 히어로 이미지
       peers_api_enabled:
         desc_html: 이 인스턴스가 페디버스에서 만났던 도메인 네임들
         title: 발견 된 인스턴스들의 리스트 발행
@@ -291,6 +294,9 @@ ko:
         open:
           desc_html: 계정을 생성할 수 있도록 허용합니다
           title: 신규 계정 등록을 받음
+      show_known_fediverse_at_about_page:
+        desc_html: 활성화 되면 프리뷰 페이지에서 페디버스의 모든 툿을 표시합니다. 비활성화시 로컬에 있는 툿만 표시 됩니다.
+        title: 타임라인 프리뷰에 알려진 페디버스 표시하기
       show_staff_badge:
         desc_html: 유저 페이지에 스태프 배지를 표시합니다
         title: 스태프 배지 표시
@@ -355,7 +361,7 @@ ko:
     your_token: 액세스 토큰
   auth:
     agreement_html: 이 등록으로 <a href="%{rules_path}">이용규약</a> 과 <a href="%{terms_path}">약관</a>에 동의하는 것으로 간주됩니다.
-    change_password: 보안
+    confirm_email: 확인 메일 승인
     delete_account: 계정 삭제
     delete_account_html: 계정을 삭제하고 싶은 경우, <a href="%{path}">여기서</a> 삭제할 수 있습니다. 삭제 전 확인 화면이 표시됩니다.
     didnt_get_confirmation: 확인 메일을 받지 못하셨습니까?
@@ -365,9 +371,12 @@ ko:
     logout: 로그아웃
     migrate_account: 계정 옮기기
     migrate_account_html: 이 계정을 다른 계정으로 리디렉션 하길 원하는 경우 <a href="%{path}">여기</a>에서 설정할 수 있습니다.
+    or: 또는
+    or_log_in_with: 다른 방법으로 로그인 하려면
     register: 등록하기
     resend_confirmation: 확인 메일을 다시 보내기
     reset_password: 비밀번호 재설정
+    security: 보안
     set_new_password: 새 비밀번호
   authorize_follow:
     error: 리모트 계정을 확인하는 도중 오류가 발생했습니다
@@ -414,6 +423,13 @@ ko:
       title: 이 페이지는 잘못되었습니다
     noscript_html: 마스토돈을 사용하기 위해서는 자바스크립트를 켜 주십시오. 아니면 <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">네이티브 앱</a> 중 하나를 사용할 수 있습니다.
   exports:
+    archive_takeout:
+      date: 날짜
+      download: 아카이브 다운로드
+      hint_html: 당신의 <strong>툿과 업로드 된 미디어</strong>의 아카이브를 요청할 수 있습니다. 내보내지는 데이터는 ActivityPub 포맷입니다. 호환 되는 모든 소프트웨어에서 읽을 수 있습니다.
+      in_progress: 당신의 아카이브를 컴파일 중입니다…
+      request: 아카이브 요청하기
+      size: 크기
     blocks: 차단
     csv: CSV
     follows: 팔로우
@@ -724,6 +740,10 @@ ko:
     setup: 초기 설정
     wrong_code: 코드가 올바르지 않습니다. 서버와 휴대전화 간의 시간이 일치하는지 확인해 주십시오.
   user_mailer:
+    backup_ready:
+      explanation: 당신이 요청한 계정의 풀 백업이 이제 다운로드 가능합니다.
+      subject: 당신의 아카이브를 다운로드 가능합니다
+      title: 아카이브 테이크 아웃
     welcome:
       edit_profile_action: 프로필 설정
       edit_profile_step: 아바타, 헤더를 업로드하고, 사람들에게 표시 될 이름을 바꾸는 것으로 당신의 프로필을 커스텀 할 수 있습니다. 사람들이 당신을 팔로우 하기 전에 리뷰를 거치게 하고 싶다면 계정을 잠그면 됩니다.
@@ -745,4 +765,5 @@ ko:
   users:
     invalid_email: 메일 주소가 올바르지 않습니다
     invalid_otp_token: 2단계 인증 코드가 올바르지 않습니다
+    seamless_external_login: 외부 서비스를 이용해 로그인 했습니다, 패스워드와 이메일 설정을 할 수 없습니다.
     signed_in_as: '다음과 같이 로그인 중:'
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index d964742ab..be8ebb637 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -273,6 +273,9 @@ nl:
       contact_information:
         email: Vul een openbaar gebruikt e-mailadres in
         username: Vul een gebruikersnaam in
+      hero:
+        desc_html: Wordt op de voorpagina getoond. Tenminste 600x100px aanbevolen. Wanneer dit niet is ingesteld wordt de thumbnail van de Mastodonserver getoond
+        title: Hero-afbeelding
       peers_api_enabled:
         desc_html: Domeinnamen die deze server in de fediverse is tegengekomen
         title: Lijst van bekende servers publiceren
@@ -309,7 +312,7 @@ nl:
         desc_html: Gebruikt als voorvertoning voor OpenGraph en de API. 1200x630px aanbevolen
         title: Thumbnail Mastodonserver
       timeline_preview:
-        desc_html: Toon een openbare tijdlijn op de landingspagina
+        desc_html: Toon een openbare tijdlijn op de voorpagina
         title: Tijdlijn als voorbeeld tonen
       title: Server-instellingen
     statuses:
@@ -356,7 +359,6 @@ nl:
     your_token: Jouw toegangscode
   auth:
     agreement_html: Wanneer je op registreren klikt ga je akkoord met het opvolgen van <a href="%{rules_path}">de regels van deze server</a> en <a href="%{terms_path}">onze gebruikersvoorwaarden</a>.
-    change_password: Beveiliging
     confirm_email: E-mail bevestigen
     delete_account: Account verwijderen
     delete_account_html: Wanneer je jouw account graag wilt verwijderen, kan je dat <a href="%{path}">hier doen</a>. We vragen jou daar om een bevestiging.
@@ -367,6 +369,7 @@ nl:
     logout: Afmelden
     migrate_account: Naar een andere account verhuizen
     migrate_account_html: Wanneer je dit account naar een ander account wilt doorverwijzen, kun je <a href="%{path}">dit hier instellen</a>.
+    or: of
     or_log_in_with: Of aanmelden met
     providers:
       cas: CAS
@@ -374,6 +377,7 @@ nl:
     register: Registreren
     resend_confirmation: Verstuur de bevestigingsinstructies nogmaals
     reset_password: Wachtwoord opnieuw instellen
+    security: Beveiliging
     set_new_password: Nieuw wachtwoord instellen
   authorize_follow:
     error: Helaas, er is een fout opgetreden bij het opzoeken van de externe account
@@ -420,6 +424,13 @@ nl:
       title: Er is iets mis
     noscript_html: Schakel JavaScript in om de webapp van Mastodon te kunnen gebruiken. Als alternatief kan je een <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">Mastodon-app</a> zoeken voor jouw platform.
   exports:
+    archive_takeout:
+      date: Datum
+      download: Jouw archief downloaden
+      hint_html: Je kunt een archief opvragen van jouw <strong>toots en geüploade media</strong>. De geëxporteerde gegevens zijn in ActivityPub-formaat, dat door hiervoor geschikte software valt uit te lezen.
+      in_progress: Jouw archief wordt samengesteld...
+      request: Jouw archief opvragen
+      size: Omvang
     blocks: Jij blokkeert
     csv: CSV
     follows: Jij volgt
@@ -535,7 +546,9 @@ nl:
           trillion: T
           unit: " "
   pagination:
+    newer: Nieuwer
     next: Volgende
+    older: Ouder
     prev: Vorige
     truncate: "&hellip;"
   preferences:
@@ -730,6 +743,10 @@ nl:
     setup: Instellen
     wrong_code: De ingevoerde code is ongeldig! Klopt de systeemtijd van de server en die van jouw apparaat?
   user_mailer:
+    backup_ready:
+      explanation: Je hebt een volledige back-up van jouw Mastodon-account opgevraagd. Het staat nu klaar om te worden gedownload!
+      subject: Jouw archief staat klaar om te worden gedownload
+      title: Archief ophalen
     welcome:
       edit_profile_action: Profiel instellen
       edit_profile_step: Je kunt jouw profiel aanpassen door een avatar (profielfoto) en omslagfoto te uploaden, jouw weergavenaam in te stellen en iets over jezelf te vertellen. Wanneer je nieuwe volgers eerst wilt goedkeuren, kun je jouw account besloten maken.
@@ -751,4 +768,5 @@ nl:
   users:
     invalid_email: E-mailadres is ongeldig
     invalid_otp_token: Ongeldige tweestaps-aanmeldcode
+    seamless_external_login: Je bent ingelogd via een externe dienst, daarom zijn wachtwoorden en e-mailinstellingen niet beschikbaar.
     signed_in_as: 'Ingelogd als:'
diff --git a/config/locales/no.yml b/config/locales/no.yml
index d198177cd..3adf71bee 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -353,7 +353,6 @@
     your_token: Din tilgangsnøkkel
   auth:
     agreement_html: Ved å registrere deg godtar du å følge <a href="%{rules_path}">instansens regler</a> og <a href="%{terms_path}">våre brukervilkår</a>.
-    change_password: Sikkerhet
     delete_account: Slett konto
     delete_account_html: Hvis du ønsker å slette din konto kan du <a href="%{path}">fortsette her</a>. Du vil bli spurt om bekreftelse.
     didnt_get_confirmation: Mottok du ikke instruksjoner om bekreftelse?
@@ -366,6 +365,7 @@
     register: Bli med
     resend_confirmation: Send bekreftelsesinstruksjoner på nytt
     reset_password: Nullstill passord
+    security: Sikkerhet
     set_new_password: Sett nytt passord
   authorize_follow:
     error: Uheldigvis skjedde det en feil da vi prøvde å få tak i en bruker fra en annen instans
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index 869118c08..160bbc3ed 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -353,7 +353,6 @@ oc:
     your_token: Vòstre geton d’accès
   auth:
     agreement_html: En vos marcar acceptatz <a href="%{rules_path}">las règlas de l’instància</a> e <a href="%{terms_path}">politica de confidencialitat</a>.
-    change_password: Seguretat
     delete_account: Suprimir lo compte
     delete_account_html: Se volètz suprimir vòstre compte, podètz <a href="%{path}">o far aquí</a>. Vos demandarem que confirmetz.
     didnt_get_confirmation: Avètz pas recebut las instruccions de confirmacion ?
@@ -369,6 +368,7 @@ oc:
     register: Se marcar
     resend_confirmation: Tornar mandar las instruccions de confirmacion
     reset_password: Reïnicializar lo senhal
+    security: Seguretat
     set_new_password: Picar un nòu senhal
   authorize_follow:
     error: O planhèm, i a agut una error al moment de cercar lo compte
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index e51f1be6b..c11e90462 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -360,7 +360,6 @@ pl:
     your_token: Twój token dostępu
   auth:
     agreement_html: Rejestrując się, oświadczasz, że zapoznałeś się z <a href="%{rules_path}">informacjami o instancji</a> i <a href="%{terms_path}">zasadami korzystania z usługi</a>.
-    change_password: Bezpieczeństwo
     confirm_email: Potwierdź adres e-mail
     delete_account: Usunięcie konta
     delete_account_html: Jeżeli chcesz usunąć konto, <a href="%{path}">przejdź tutaj</a>. Otrzymasz prośbę o potwierdzenie.
@@ -371,6 +370,7 @@ pl:
     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>.
+    or: lub
     or_log_in_with: Lub zaloguj się z użyciem
     providers:
       cas: CAS
@@ -378,6 +378,7 @@ pl:
     register: Rejestracja
     resend_confirmation: Ponownie prześlij instrukcje weryfikacji
     reset_password: Zresetuj hasło
+    security: Bezpieczeństwo
     set_new_password: Ustaw nowe hasło
   authorize_follow:
     error: Niestety, podczas sprawdzania zdalnego konta wystąpił błąd
@@ -561,7 +562,9 @@ pl:
           trillion: T
           unit: ''
   pagination:
+    newer: Nowsze
     next: Następna
+    older: Starsze
     prev: Poprzednia
     truncate: "&hellip;"
   preferences:
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index f51abeff2..72ef6cbb6 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -273,6 +273,9 @@ pt-BR:
       contact_information:
         email: E-mail
         username: Contate usuário
+      hero:
+        desc_html: Aparece na página inicial. Ao menos 600x100px é recomendado. Se não estiver definido, o thumbnail da instância é usado no lugar
+        title: Imagem do herói
       peers_api_enabled:
         desc_html: Nomes de domínio que essa instância encontrou no fediverso
         title: Publicar lista de instâncias descobertas
@@ -356,7 +359,7 @@ pt-BR:
     your_token: Seu token de acesso
   auth:
     agreement_html: Ao se cadastrar você concorda em seguir <a href="%{rules_path}">as regras da instância</a> e <a href="%{terms_path}">os nossos termos de serviço</a>.
-    change_password: Segurança
+    confirm_email: Confirmar e-mail
     delete_account: Excluir conta
     delete_account_html: Se você deseja excluir a sua conta, você pode <a href="%{path}">prosseguir para cá</a>. Uma confirmação será requisitada.
     didnt_get_confirmation: Não recebeu instruções de confirmação?
@@ -366,9 +369,15 @@ pt-BR:
     logout: Sair
     migrate_account: Mudar para uma conta diferente
     migrate_account_html: Se você quer redirecionar essa conta para uma outra você pode <a href="%{path}">configurar isso aqui</a>.
+    or: ou
+    or_log_in_with: Ou faça login com
+    providers:
+      cas: CAS
+      saml: SAML
     register: Cadastrar-se
     resend_confirmation: Reenviar instruções de confirmação
     reset_password: Redefinir senha
+    security: Segurança
     set_new_password: Definir uma nova senha
   authorize_follow:
     error: Infelizmente, ocorreu um erro ao buscar a conta remota
@@ -415,6 +424,13 @@ pt-BR:
       title: Esta página não está certa
     noscript_html: Para usar o aplicativo web do Mastodon, por favor ative o JavaScript. Ou, se quiser, experimente um dos <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">apps nativos</a> para o Mastodon em sua plataforma.
   exports:
+    archive_takeout:
+      date: Data
+      download: Baixe o seu arquivo
+      hint_html: Você pode pedir um arquivo dos seus <strong>toots e mídia enviada</strong>. Os dados exportados estarão no formato ActivityPub, que podem ser lidos por qualquer software compatível.
+      in_progress: Preparando seu arquivo...
+      request: Solicitar o seu arquivo
+      size: Tamanho
     blocks: Você bloqueou
     csv: CSV
     follows: Você segue
@@ -530,7 +546,9 @@ pt-BR:
           trillion: T
           unit: ''
   pagination:
+    newer: Mais novo
     next: Próximo
+    older: Mais antigo
     prev: Anterior
     truncate: "&hellip;"
   preferences:
@@ -725,6 +743,10 @@ pt-BR:
     setup: Configurar
     wrong_code: O código inserido é invalido! O horário do servidor e o horário do seu aparelho estão corretos?
   user_mailer:
+    backup_ready:
+      explanation: Você pediu um backup completo da sua conta no Mastodon. E agora está pronto para ser baixado!
+      subject: Seu arquivo está pronto para ser baixado
+      title: Arquivo "pra viagem"
     welcome:
       edit_profile_action: Configurar perfil
       edit_profile_step: Você pode customizar o seu perfil enviando um avatar, uma imagem de topo, mudando seu nome de exibição, dentre outros. Se você gostaria de aprovar novos seguidores antes que eles possam seguir você, você pode trancar a sua conta.
@@ -746,4 +768,5 @@ pt-BR:
   users:
     invalid_email: O endereço de e-mail é inválido
     invalid_otp_token: Código de autenticação inválido
+    seamless_external_login: Você está logado usando um serviço externo, então configurações de e-mail e password não estão disponíveis.
     signed_in_as: 'Acesso como:'
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index 455f27898..5012e176f 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -353,7 +353,6 @@ pt:
     your_token: O teu token de acesso
   auth:
     agreement_html: Registando-te concordas em seguir <a href="%{rules_path}">as regras da instância</a> e <a href="%{terms_path}">os nossos termos de serviço</a>.
-    change_password: Alterar palavra-passe
     confirm_email: Confirmar e-mail
     delete_account: Eliminar conta
     delete_account_html: Se desejas eliminar a conta, podes <a href="%{path}">continua aqui</a>. Uma confirmação será pedida.
@@ -367,6 +366,7 @@ pt:
     register: Registar
     resend_confirmation: Reenviar instruções de confirmação
     reset_password: Criar nova palavra-passe
+    security: Alterar palavra-passe
     set_new_password: Editar palavra-passe
   authorize_follow:
     error: Infelizmente, ocorreu um erro ao buscar a conta remota
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 467f24ca8..a2cb1e793 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -353,7 +353,6 @@ ru:
     your_token: Ваш токен доступа
   auth:
     agreement_html: Создавая аккаунт, вы соглашаетесь с <a href="%{rules_path}">правилами узла</a> и <a href="%{terms_path}">нашими условиями обслуживания</a>.
-    change_password: Изменить пароль
     delete_account: Удалить аккаунт
     delete_account_html: Если Вы хотите удалить свой аккаунт, вы можете <a href="%{path}">перейти сюда</a>. У Вас будет запрошено подтверждение.
     didnt_get_confirmation: Не получили инструкцию для подтверждения?
@@ -366,6 +365,7 @@ ru:
     register: Зарегистрироваться
     resend_confirmation: Повторить отправку инструкции для подтверждения
     reset_password: Сбросить пароль
+    security: Изменить пароль
     set_new_password: Задать новый пароль
   authorize_follow:
     error: К сожалению, при поиске удаленного аккаунта возникла ошибка
diff --git a/config/locales/simple_form.ar.yml b/config/locales/simple_form.ar.yml
index abed908fb..2459f6da6 100644
--- a/config/locales/simple_form.ar.yml
+++ b/config/locales/simple_form.ar.yml
@@ -5,14 +5,8 @@ ar:
       defaults:
         avatar: ملف PNG أو GIF أو JPG. حجمه على أقصى تصدير 2MB. سيتم تصغيره إلى 120x120px
         digest: تُرسَل إليك بعد مُضيّ مدة مِن خمول نشاطك و فقط إذا ما تلقيت رسائل شخصية مباشِرة أثناء فترة غيابك مِن الشبكة
-        display_name:
-          one: <span class="name-counter">1</span> حرف متبقي
-          other: <span class="name-counter">%{count}</span> حروف متبقية
         header: ملف PNG أو GIF أو JPG. حجمه على أقصى تصدير 2MB. سيتم تصغيره إلى 700x335px
         locked: يتطلب منك الموافقة يدويا على طلبات المتابعة
-        note:
-          one: <span class="note-counter">1</span> حرف متبقي
-          other: <span class="note-counter">%{count}</span> حروف متبقية
         setting_noindex: ذلك يؤثر على حالة ملفك الشخصي و صفحاتك
         setting_theme: ذلك يؤثر على الشكل الذي سيبدو عليه ماستدون عندما تقوم بالدخول مِن أي جهاز.
       imports:
diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml
index 2bb0215d5..dfdd05b05 100644
--- a/config/locales/simple_form.eo.yml
+++ b/config/locales/simple_form.eo.yml
@@ -44,7 +44,7 @@ eo:
         setting_boost_modal: Montri fenestron por konfirmi antaŭ ol diskonigi
         setting_default_privacy: Mesaĝa videbleco
         setting_default_sensitive: Ĉiam marki aŭdovidaĵojn tiklaj
-        setting_delete_modal: Montri fenestron por konfirmi antaŭ ol forigi hupon
+        setting_delete_modal: Montri fenestron por konfirmi antaŭ ol forigi mesaĝon
         setting_display_sensitive_media: Ĉiam montri aŭdovidaĵon markitajn tiklaj
         setting_noindex: Ellistiĝi de retserĉila indeksado
         setting_reduce_motion: Malrapidigi animaciojn
diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml
index 2ed0e3329..85dc418bd 100644
--- a/config/locales/simple_form.pt-BR.yml
+++ b/config/locales/simple_form.pt-BR.yml
@@ -45,6 +45,7 @@ pt-BR:
         setting_default_privacy: Privacidade das postagens
         setting_default_sensitive: Sempre marcar mídia como sensível
         setting_delete_modal: Mostrar diálogo de confirmação antes de deletar uma postagem
+        setting_display_sensitive_media: Sempre mostrar mídia marcada como sensível
         setting_noindex: Não quero ser indexado por mecanismos de busca
         setting_reduce_motion: Reduz movimento em animações
         setting_system_font_ui: Usar a fonte padrão de seu sistema
@@ -53,6 +54,7 @@ pt-BR:
         severity: Gravidade
         type: Tipo de importação
         username: Nome de usuário
+        username_or_email: Nome de usuário ou e-mail
       interactions:
         must_be_follower: Bloquear notificações de não-seguidores
         must_be_following: Bloquear notificações de pessoas que você não segue
diff --git a/config/locales/simple_form.sk.yml b/config/locales/simple_form.sk.yml
index 1ef8f2c80..7312f565c 100644
--- a/config/locales/simple_form.sk.yml
+++ b/config/locales/simple_form.sk.yml
@@ -37,8 +37,8 @@ sk:
         locked: Zamknúť účet
         max_uses: Maximálne možno použiť
         new_password: Nové heslo
-        note: O vás
-        otp_attempt: Dvoj-faktorový (2FA) kód
+        note: O tebe
+        otp_attempt: Dvoj-faktorový overovací (2FA) kód
         password: Heslo
         setting_auto_play_gif: Automaticky prehrávať animované GIFy
         setting_boost_modal: Zobrazovať potvrdzovacie okno pred re-toot
@@ -56,16 +56,16 @@ sk:
         username: Používateľské meno
         username_or_email: Prezívka, alebo Email
       interactions:
-        must_be_follower: Blokovať notifikácie pod používateľov, ktorí vás nesledujú
-        must_be_following: Blokovať notifikácie od ľudí ktorý vás nesledujú
+        must_be_follower: Blokovať notifikácie pod používateľov, ktorí ťa nesledujú
+        must_be_following: Blokovať notifikácie od ľudí ktorí ťa nesledujú
         must_be_following_dm: Blokovať súkromné správy od ľudí ktorých nesleduješ
       notification_emails:
         digest: Posielať súhrnné emaily
         favourite: Poslať email ak niekto označí váš príspevok ako obľúbený
-        follow: Poslať email ak vás niekto začne sledovať
-        follow_request: Poslať email ak vám niekto pošle žiadosť o sledovanie
-        mention: Poslať email ak vás niekto spomenie v svojom príspevku
-        reblog: Poslať email ak niekto re-tootne váš príspevok
+        follow: Poslať email, ak ťa niekto začne následovať
+        follow_request: Zaslať email ak ti niekto pošle žiadosť o sledovanie
+        mention: Poslať email ak ťa niekto spomenie v svojom príspevku
+        reblog: Poslať email ak niekto re-tootne tvoj príspevok
     'no': Nie
     required:
       mark: "*"
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index 5e5dd42e9..4fa745d5a 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -1,7 +1,7 @@
 ---
 sk:
   about:
-    about_hashtag_html: Toto sú verejné tooty otagované <strong>#%{tagom}</strong>. Ak máš účet niekde vo fediverse, môžeš ich používať.
+    about_hashtag_html: Toto sú verejné toot príspevky otagované <strong>#%{tagom}</strong>. Ak máš účet niekde vo fediverse, môžeš ich používať.
     about_mastodon_html: Mastodon je sociálna sieť založená na otvorených webových protokoloch. Jej zrojový kód je otvorený a je decentralizovaná podobne ako email.
     about_this: O instancii
     closed_registrations: Registrácie sú momentálne uzatvorené. Avšak, existujú ďalšie Mastodon inštancie kde si môžete založiť účet a získať prístup do tej istej siete z tamaď.
@@ -43,11 +43,11 @@ sk:
     people_followed_by: Ľudia, ktorých %{name} sleduje
     people_who_follow: Ľudia sledujúci %{name}
     posts: Tooty
-    posts_with_replies: Tooty s odpoveďami
-    remote_follow: Vzdialené sledovanie
+    posts_with_replies: Toot príspevky s odpoveďami
+    remote_follow: Sleduj vzdialeného
     reserved_username: Prihlasovacie meno je rezervované
     roles:
-      admin: Admin
+      admin: Administrátor
       moderator: Moderátor
     unfollow: Prestať sledovať
   admin:
@@ -273,6 +273,9 @@ sk:
       contact_information:
         email: Pracovný e-mail
         username: Kontaktné užívateľské meno
+      hero:
+        desc_html: Zobrazuje sa na hlavnej stránke. Doporučuje sa rozlišenie aspoň 600x100px Pokiaľ tu nieje nič dodané, bude nastavený základný orázok tohoto serveru
+        title: Obrázok hrdinu
       peers_api_enabled:
         desc_html: Domény na ktoré táto instancia už vo fediverse natrafila
         title: Zverejniť zoznam objavených instancií
@@ -290,7 +293,7 @@ sk:
           desc_html: Povoliť každému aby si mohli vytvoriť účet
           title: Verejná registrácia
       show_known_fediverse_at_about_page:
-        desc_html: Pokiaľ je zapnuté, bude v ukážke osi možné nahliadnúť statusy z celého známeho fediversa. V opačnom prípade tam budú ukázané iba statusy z lokálnej osi.
+        desc_html: Pokiaľ je zapnuté, bude v ukážke osi možné nahliadnúť toot statusy z celého známeho fediversa. V opačnom prípade tam budú ukázané iba statusy z lokálnej osi.
         title: Ukázať celé známe fediversum ako ukážku osi
       show_staff_badge:
         desc_html: Zobraziť moderátorsku značku na užívateľovej stránke
@@ -356,7 +359,6 @@ sk:
     your_token: Váš prístupový token
   auth:
     agreement_html: V rámci registrácie súhlasíte, že sa budete riadiť  <a href="%{rules_path}"> 1 pravidlami tejto instancie</a> 2 a taktiež <a href="%{terms_path}"> 3 našími servisnými podmienkami </a> 4.
-    change_password: Zabezpečenie
     confirm_email: Potvrdiť email
     delete_account: Vymazať účet
     delete_account_html: Pokiaľ si želáte vymazať svoj účet, môžete tak <a href="%{path}"> 1 urobiť tu</a> 2. Budete požiadaný/á o potvrdenie tohto kroku.
@@ -367,6 +369,7 @@ sk:
     logout: Odhlásiť sa
     migrate_account: Presunúť sa na iný účet
     migrate_account_html: Pokiaľ si želáte presmerovať tento účet na nejaký iný, môžete <a href="%{path}"> tak urobiť tu</a>.
+    or: alebo
     or_log_in_with: Alebo prihlásiť z
     providers:
       cas: CAS
@@ -374,6 +377,7 @@ sk:
     register: Zaregistrovať sa
     resend_confirmation: Poslať potvrdzujúce pokyny znovu
     reset_password: Resetovať heslo
+    security: Zabezpečenie
     set_new_password: Nastaviť nové heslo
   authorize_follow:
     error: Naneštastie nastala chyba pri hľadaní vzdialeného účtu
@@ -404,7 +408,7 @@ sk:
     description_html: Týmto <strong> natrvalo, nenavrátiteľne </strong> vymažeš obsah tvojho účtu, a deaktivuješ ho. Tvoja prezývka ale ostane rezervovaná ako prevencia pred budúcimi impersonáciami.
     proceed: Vymazať účet
     success_msg: Váš účet bol úspešne vymazaný
-    warning_html: Iba vymazanie obsahu z tejto konkrétnej instancie je garantované. Obsah ktorý bol zdieľaný široko=ďaleko pravdepodobne zanechá nejaké stopy. Servery ktoré sú offline a tie ktoré vás ignorujú nezaktualizujú svoje databázy.
+    warning_html: Iba vymazanie obsahu z tejto konkrétnej instancie je garantované. Obsah ktorý bol zdieľaný široko-ďaleko pravdepodobne zanechá nejaké stopy. Servery ktoré sú offline a tie ktoré ignorujú tvoje zmeny teda nezaktualizujú svoje databázy.
     warning_title: Dostupnosť distribuovaného obsahu
   errors:
     '403': Nemáte dostatočné povolenie na zobrazenie tejto stránky.
@@ -419,6 +423,13 @@ sk:
       title: Táto stránka nieje v poriadku
     noscript_html: Aby bolo možné používať Mastodon web aplikáciu, prosím povoľte JavaScript. Alebo skúste jednu z <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md"> aplikácii </a> dostupných pre vašu platformu.
   exports:
+    archive_takeout:
+      date: Dátum
+      download: Stiahni si svoj archív
+      hint_html: Môžeš si opýtať <strong>archív svojích príspevkov a nahratých médií</strong>. Exportované dáta budú v ActivityPub formáte, čítateľné hociakým kompatibilným softvérom.
+      in_progress: Balím tvoj archív...
+      request: Vyžiadaj si tvoj archív
+      size: Veľkosť
     blocks: Blokujete
     csv: CSV
     follows: Následujete
@@ -518,9 +529,99 @@ sk:
       body: "%{name} ťa spomenul/a v:"
       subject: Boli ste spomenutí užívateľom %{name}
       title: Nové spomenutie
+    reblog:
+      body: 'Tvoj príspevok bol pozdvihnutý užívateľom %{name}:'
+      subject: "%{name} pozdvihli tvoj príspevok"
+      title: Novo pozdvyhnuté
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+  pagination:
+    newer: Novšie
+    next: Ďalšie
+    older: Staršie
+    prev: Predošlé
+    truncate: "&hellip;"
+  preferences:
+    languages: Jazyky
+    other: Ostatné
+    publishing: Publikovanie
+    web: Web
+  push_notifications:
+    favourite:
+      title: "%{name} si obľúbil/a tvoj príspevok"
+    follow:
+      title: "%{name} ťa teraz následuje"
+    group:
+      title: "%{count} notifikácie"
+    mention:
+      action_boost: Pozdvihni
+      action_expand: Ukáž viac
+      action_favourite: Obľúbené
+      title: "%{name} ťa spomenul/a"
+    reblog:
+      title: "%{name} vyzdvihli tvoj príspevok"
+  remote_follow:
+    acct: Napíš svoju prezývku@doménu z ktorej chceš následovať
+    missing_resource: Nemôžeme nájsť potrebnú presmerovaciu adresu k tvojmu účtu
+    proceed: Začni následovať
+    prompt: 'Budeš sledovať:'
+  sessions:
+    activity: Najnovšia aktivita
+    browser: Prehliadač
+    browsers:
+      alipay: Alipay
+      generic: Neznámy prehliadač
+    current_session: Aktuálna sezóna
+    description: "%{browser} na %{platform}"
+    explanation: Tieto sú prehliadače ktoré sú teraz prihlásené na tvoj Mastodon účet.
+    ip: IP adresa
+    platforms:
+      mac: MacOSX
+      other: neznáma platforma
+    revoke: Zamietni
+    revoke_success: Sezóna úspešne zamietnutá
+    title: Sezóna
   settings:
     authorized_apps: Autorizované aplikácie
     back: Naspäť na stránku
+    delete: Zmazanie účtu
+    development: Vývoj
+    edit_profile: Upraviť profil
+    export: Exportovať dáta
+    followers: Povolení sledovatelia
+    import: Importovať
+    migrate: Presunúť účet
+    notifications: Oznámenia
+    preferences: Možnosti
+    settings: Nastavenia
+    two_factor_authentication: Dvoj-faktorové overenie
+    your_apps: Tvoje aplikácie
+  statuses:
+    open_in_web: Otvor v okne prehliadača
+    over_character_limit: limit počtu %{max} znakov bol presiahnutý
+    pin_errors:
+      ownership: Nemožno pripnúť príspevok od niekoho iného
+      private: Neverejné príspevky nemôžu byť pripnuté
+    show_more: Ukáž viac
+    visibilities:
+      private: Iba pre sledovateľov
+      private_long: Ukáž iba následovateľom
+      public: Verejné
+      public_long: Všetci môžu vidieť
+      unlisted: Nezaradené
+      unlisted_long: Všetci môžu vidieť, ale nieje zaradené do verejnej osi
+  stream_entries:
+    click_to_show: Klikni pre zobrazenie
+    pinned: Pripnutý toot
+    reblogged: vyzdvihnutý
   user_mailer:
     welcome:
       final_step: 'Začnite písať! Aj bez následovníkov budú vaše verejné správy videné ostatnými, napríklad na lokálnej osi a pod haštagmi. Môžete sa ostatným predstaviť pod haštagom #introductions.'
diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml
index 4eed44345..2d984049a 100644
--- a/config/locales/sr-Latn.yml
+++ b/config/locales/sr-Latn.yml
@@ -346,7 +346,6 @@ sr-Latn:
     your_token: Vaš pristupni token
   auth:
     agreement_html: Pristupanjem instanci se slažete sa <a href="%{rules_path}">pravilima instance</a> i <a href="%{terms_path}">uslovima korišćenja</a>.
-    change_password: Bezbednost
     delete_account: Obriši nalog
     delete_account_html: Ako želite da obrišete Vaš nalog, možete <a href="%{path}">nastaviti ovde</a>. Bićete upitani da potvrdite.
     didnt_get_confirmation: Niste dobili poruku sa uputstvima za potvrdu naloga?
@@ -359,6 +358,7 @@ sr-Latn:
     register: Registruj se
     resend_confirmation: Pošalji poruku sa uputstvima o potvrdi naloga ponovo
     reset_password: Resetuj lozinku
+    security: Bezbednost
     set_new_password: Postavi novu lozinku
   authorize_follow:
     error: Nažalost, desila se greška pri traženju udaljenog naloga
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index c56498765..2daf32915 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -346,7 +346,6 @@ sr:
     your_token: Ваш приступни токен
   auth:
     agreement_html: Приступањем инстанци се слажете са <a href="%{rules_path}">правилима инстанце</a> и <a href="%{terms_path}">условима коришћења</a>.
-    change_password: Безбедност
     delete_account: Обриши налог
     delete_account_html: Ако желите да обришете Ваш налог, можете <a href="%{path}">наставити овде</a>. Бићете упитани да потврдите.
     didnt_get_confirmation: Нисте добили поруку са упутствима за потврду налога?
@@ -359,6 +358,7 @@ sr:
     register: Региструј се
     resend_confirmation: Пошаљи поруку са упутствима о потврди налога поново
     reset_password: Ресетуј лозинку
+    security: Безбедност
     set_new_password: Постави нову лозинку
   authorize_follow:
     error: Нажалост, десила се грешка при тражењу удаљеног налога
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index 1e79b63e5..8d9c6d5df 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -356,7 +356,6 @@ sv:
     your_token: Din access token
   auth:
     agreement_html: Genom att registrera dig godkänner du att följa <a href="%{rules_path}">instansens regler</a> och <a href="%{terms_path}">våra användarvillkor</a>.
-    change_password: Säkerhet
     confirm_email: Bekräfta e-postadress
     delete_account: Ta bort konto
     delete_account_html: Om du vill radera ditt konto kan du <a href="%{path}">fortsätta här</a>. Du kommer att bli ombedd att bekräfta.
@@ -374,6 +373,7 @@ sv:
     register: Registrera
     resend_confirmation: Skicka instruktionerna om bekräftelse igen
     reset_password: Återställ lösenord
+    security: Säkerhet
     set_new_password: Skriv in nytt lösenord
   authorize_follow:
     error: Tyvärr inträffade ett fel när vi kontrollerade fjärrkontot
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 737b3aa95..45fe1e475 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -157,7 +157,6 @@ th:
   applications:
     invalid_url: URL ที่ระบุไม่ถูกตั้ง
   auth:
-    change_password: Credentials
     didnt_get_confirmation: Didn't receive confirmation instructions?
     forgot_password: คุณลืมพาสเวริ์ดใช่ัม้ย?
     login: ล๊อคอิน
@@ -165,6 +164,7 @@ th:
     register: สมัคร
     resend_confirmation: ส่งขั้นตอนวิธีการยืนยันใหม่อีกครั้ง
     reset_password: เปลี่ยนรหัสผ่าน
+    security: Credentials
     set_new_password: ตั้งรหัสผ่านใหม่
   authorize_follow:
     error: Unfortunately, there was an error looking up the remote account
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index 23b4d7a24..ee0e33074 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -156,7 +156,6 @@ tr:
   applications:
     invalid_url: Verilen URL geçerli değil
   auth:
-    change_password: Kimlik bilgileri
     didnt_get_confirmation: Hesap doğrulama mailini almadınız mı?
     forgot_password: Parolanızı unuttunuz mu?
     login: Giriş yap
@@ -164,6 +163,7 @@ tr:
     register: Üye ol
     resend_confirmation: Doğrulama mailini tekrar gönder
     reset_password: Parolayı değiştir
+    security: Kimlik bilgileri
     set_new_password: Yeni parola oluştur
   authorize_follow:
     error: Uzak hesap aranırken bir hata oluştu.
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 0ddfa9190..4c1c66b31 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -147,7 +147,6 @@ uk:
   applications:
     invalid_url: Введена URL неправильна
   auth:
-    change_password: Зміна паролю
     didnt_get_confirmation: Ви не отримали інструкції з підтвердження?
     forgot_password: Забули свій пароль?
     login: Увійти
@@ -155,6 +154,7 @@ uk:
     register: Зареєструватися
     resend_confirmation: Повторно відправити інструкції з підтвердження
     reset_password: Скинути пароль
+    security: Зміна паролю
     set_new_password: Встановити новий пароль
   authorize_follow:
     error: На жаль, при пошуку віддаленого аккаунту виникла помилка
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 1bd2e5039..1254651cd 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -351,7 +351,6 @@ zh-CN:
     your_token: 你的访问令牌
   auth:
     agreement_html: 注册即表示你同意遵守<a href="%{rules_path}">本实例的相关规定</a>和<a href="%{terms_path}">我们的使用条款</a>。
-    change_password: 帐户安全
     delete_account: 删除帐户
     delete_account_html: 如果你想删除你的帐户,请<a href="%{path}">点击这里继续</a>。你需要确认你的操作。
     didnt_get_confirmation: 没有收到确认邮件?
@@ -364,6 +363,7 @@ zh-CN:
     register: 注册
     resend_confirmation: 重新发送确认邮件
     reset_password: 重置密码
+    security: 帐户安全
     set_new_password: 设置新密码
   authorize_follow:
     error: 对不起,寻找这个跨站用户时出错
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index ed73b7244..e7ab347a1 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -156,7 +156,6 @@ zh-HK:
   applications:
     invalid_url: 所提供的網址不正確
   auth:
-    change_password: 登入資訊
     didnt_get_confirmation: 沒有收到確認指示電郵?
     forgot_password: 忘記了密碼?
     login: 登入
@@ -164,6 +163,7 @@ zh-HK:
     register: 登記
     resend_confirmation: 重發確認指示電郵
     reset_password: 重設密碼
+    security: 登入資訊
     set_new_password: 設定新密碼
   authorize_follow:
     error: 對不起,尋找這個跨站用戶的過程發生錯誤
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index bd9f85840..2fec09ed8 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -127,7 +127,6 @@ zh-TW:
   applications:
     invalid_url: 網址不正確
   auth:
-    change_password: 登入資訊
     didnt_get_confirmation: 沒有收到驗證信?
     forgot_password: 忘記密碼?
     login: 登入
@@ -137,6 +136,7 @@ zh-TW:
     register: 註冊
     resend_confirmation: 重寄驗證信
     reset_password: 重設密碼
+    security: 登入資訊
     set_new_password: 設定新密碼
   authorize_follow:
     error: 對不起,搜尋遠端使用者出現錯誤
diff --git a/config/navigation.rb b/config/navigation.rb
index b08b1769d..d80546c35 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -9,7 +9,7 @@ SimpleNavigation::Configuration.run do |navigation|
       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
-      settings.item :password, safe_join([fa_icon('lock fw'), t('auth.change_password')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete}
+      settings.item :password, safe_join([fa_icon('lock fw'), t('auth.security')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete}
       settings.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_url, highlights_on: %r{/settings/two_factor_authentication}
       settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url
       settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url
diff --git a/config/routes.rb b/config/routes.rb
index 903f4553e..8fd810c10 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -58,8 +58,10 @@ Rails.application.routes.draw do
     resources :following, only: [:index], controller: :following_accounts
     resource :follow, only: [:create], controller: :account_follow
     resource :unfollow, only: [:create], controller: :account_unfollow
+
     resource :outbox, only: [:show], module: :activitypub
     resource :inbox, only: [:create], module: :activitypub
+    resources :collections, only: [:show], module: :activitypub
   end
 
   resource :inbox, only: [:create], module: :activitypub
diff --git a/db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb b/db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb
new file mode 100644
index 000000000..e0b8ed5cc
--- /dev/null
+++ b/db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb
@@ -0,0 +1,5 @@
+class AddFeaturedCollectionUrlToAccounts < ActiveRecord::Migration[5.1]
+  def change
+    add_column :accounts, :featured_collection_url, :string
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a0187d5a6..70420d396 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: 20180211015820) do
+ActiveRecord::Schema.define(version: 20180304013859) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -73,6 +73,7 @@ ActiveRecord::Schema.define(version: 20180211015820) do
     t.integer "protocol", default: 0, null: false
     t.boolean "memorial", default: false, null: false
     t.bigint "moved_to_account_id"
+    t.string "featured_collection_url"
     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"
diff --git a/docker-compose.yml b/docker-compose.yml
index 55b419e98..836cb00b8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -32,7 +32,7 @@ services:
 
   web:
     build: .
-    image: gargron/mastodon
+    image: tootsuite/mastodon
     restart: always
     env_file: .env.production
     command: bundle exec rails s -p 3000 -b '0.0.0.0'
@@ -52,10 +52,10 @@ services:
 
   streaming:
     build: .
-    image: gargron/mastodon
+    image: tootsuite/mastodon
     restart: always
     env_file: .env.production
-    command: npm run start
+    command: yarn start
     networks:
       - external_network
       - internal_network
@@ -67,7 +67,7 @@ services:
 
   sidekiq:
     build: .
-    image: gargron/mastodon
+    image: tootsuite/mastodon
     restart: always
     env_file: .env.production
     command: bundle exec sidekiq -q default -q mailers -q pull -q push
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index 4150b39b6..9155702ac 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -21,7 +21,7 @@ module Mastodon
     end
 
     def flags
-      'rc1'
+      'rc3'
     end
 
     def to_a
diff --git a/lib/paperclip/lazy_thumbnail.rb b/lib/paperclip/lazy_thumbnail.rb
index 42f9a557a..aafa21343 100644
--- a/lib/paperclip/lazy_thumbnail.rb
+++ b/lib/paperclip/lazy_thumbnail.rb
@@ -4,6 +4,10 @@ module Paperclip
   class LazyThumbnail < Paperclip::Thumbnail
     def make
       return File.open(@file.path) unless needs_convert?
+
+      min_side = [@current_geometry.width, @current_geometry.height].min
+      options[:geometry] = "#{min_side.to_i}x#{min_side.to_i}#" if @target_geometry.square? && min_side < @target_geometry.width
+
       Paperclip::Thumbnail.make(file, options, attachment)
     end
 
diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake
index 0826f0186..b642510a1 100644
--- a/lib/tasks/assets.rake
+++ b/lib/tasks/assets.rake
@@ -1,10 +1,8 @@
 # frozen_string_literal: true
 
 def render_static_page(action, dest:, **opts)
-  I18n.with_locale(ENV['DEFAULT_LOCALE'] || I18n.default_locale) do
-    html = ApplicationController.render(action, opts)
-    File.write(dest, html)
-  end
+  html = ApplicationController.render(action, opts)
+  File.write(dest, html)
 end
 
 namespace :assets do
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 9202b4839..0b011bf57 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -476,10 +476,10 @@ namespace :mastodon do
       time_ago = ENV.fetch('NUM_DAYS') { 7 }.to_i.days.ago
 
       MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).find_each do |media|
-        if media.file.exists?
-          media.file.destroy
-          media.save
-        end
+        next unless media.file.exists?
+
+        media.file.destroy
+        media.save
       end
     end
 
diff --git a/nanobox/nginx-web.conf.erb b/nanobox/nginx-web.conf.erb
index a839f3036..797201eab 100644
--- a/nanobox/nginx-web.conf.erb
+++ b/nanobox/nginx-web.conf.erb
@@ -58,15 +58,21 @@ http {
             proxy_pass_header Server;
 
             proxy_pass http://rails;
-            proxy_buffering off;
+            proxy_buffering on;
             proxy_redirect off;
             proxy_http_version 1.1;
             proxy_set_header Upgrade $http_upgrade;
             proxy_set_header Connection $connection_upgrade;
 
+            proxy_cache CACHE;
+            proxy_cache_valid 200 7d;
+            proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
+
             tcp_nodelay on;
         }
     }
 
+    proxy_cache_path /data/var/cache/nginx levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g;
+
     error_page 500 501 502 503 504 /500.html;
 }
diff --git a/package.json b/package.json
index 38b517f04..d84bfb06f 100644
--- a/package.json
+++ b/package.json
@@ -10,10 +10,9 @@
     "build:production": "cross-env RAILS_ENV=production ./bin/webpack",
     "manage:translations": "node ./config/webpack/translationRunner.js",
     "start": "node ./streaming/index.js",
-    "test": "npm run test:lint && npm run test:jest",
+    "test": "npm-run-all test:lint test:jest",
     "test:lint": "eslint -c .eslintrc.yml --ext=js app/javascript/ config/webpack/ streaming/",
-    "test:jest": "cross-env NODE_ENV=test jest --coverage",
-    "postinstall": "npm rebuild node-sass"
+    "test:jest": "cross-env NODE_ENV=test jest --coverage"
   },
   "repository": {
     "type": "git",
@@ -69,9 +68,11 @@
     "marky": "^1.2.0",
     "mkdirp": "^0.5.1",
     "node-sass": "^4.7.2",
+    "npm-run-all": "^4.1.2",
     "npmlog": "^4.1.2",
     "object-assign": "^4.1.1",
     "object-fit-images": "^3.2.3",
+    "object.values": "^1.0.4",
     "offline-plugin": "^4.8.3",
     "path-complete-extname": "^0.1.0",
     "pg": "^6.4.0",
diff --git a/spec/controllers/concerns/localized_spec.rb b/spec/controllers/concerns/localized_spec.rb
index c917ce85e..f71c96aff 100644
--- a/spec/controllers/concerns/localized_spec.rb
+++ b/spec/controllers/concerns/localized_spec.rb
@@ -16,49 +16,24 @@ describe ApplicationController, type: :controller do
   end
 
   shared_examples 'default locale' do
-    context 'when DEFAULT_LOCALE environment variable is set' do
-      around do |example|
-        ClimateControl.modify 'DEFAULT_LOCALE' => 'ca', &example.method(:run)
-        I18n.locale = I18n.default_locale
-      end
+    after { I18n.locale = I18n.default_locale }
 
-      it 'sets language specified by ENV if preferred' do
-        request.headers['Accept-Language'] = 'ca, fa'
-        get 'success'
-        expect(I18n.locale).to eq :ca
-      end
-
-      it 'sets available and preferred language if language specified by ENV is not preferred' do
-        request.headers['Accept-Language'] = 'ca-ES, fa'
-        get 'success'
-        expect(I18n.locale).to eq :fa
-      end
-
-      it 'sets language specified by ENV if it is compatible and none of available languages are preferred' do
-        request.headers['Accept-Language'] = 'ca-ES, fa-IR'
-        get 'success'
-        expect(I18n.locale).to eq :ca
-      end
-
-      it 'sets available and compatible langauge if language specified by ENV is not compatible none of available languages are preferred' do
-        request.headers['Accept-Language'] = 'fa-IR'
-        get 'success'
-        expect(I18n.locale).to eq :fa
-      end
+    it 'sets available and preferred language' do
+      request.headers['Accept-Language'] = 'ca-ES, fa'
+      get 'success'
+      expect(I18n.locale).to eq :fa
+    end
 
-      it 'sets language specified by ENV if none of available languages are compatible' do
-        request.headers['Accept-Language'] = ''
-        get 'success'
-        expect(I18n.locale).to eq :ca
-      end
+    it 'sets available and compatible langauge if none of available languages are preferred' do
+      request.headers['Accept-Language'] = 'fa-IR'
+      get 'success'
+      expect(I18n.locale).to eq :fa
     end
 
-    context 'when DEFAULT_LOCALE environment variable is not set' do
-      it 'sets default locale if none of available languages are compatible' do
-        request.headers['Accept-Language'] = ''
-        get 'success'
-        expect(I18n.locale).to eq :en
-      end
+    it 'sets default locale if none of available languages are compatible' do
+      request.headers['Accept-Language'] = ''
+      get 'success'
+      expect(I18n.locale).to eq :en
     end
   end
 
diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb
index 1077a7288..f43cf0c27 100644
--- a/spec/controllers/home_controller_spec.rb
+++ b/spec/controllers/home_controller_spec.rb
@@ -4,21 +4,24 @@ RSpec.describe HomeController, type: :controller do
   render_views
 
   describe 'GET #index' do
+    subject { get :index }
+
     context 'when not signed in' do
+      context 'when requested path is tag timeline' do
+        before { @request.path = '/web/timelines/tag/name' }
+        it { is_expected.to redirect_to '/tags/name' }
+      end
+
       it 'redirects to about page' do
         @request.path = '/'
-        get :index
-        expect(response).to redirect_to(about_path)
+        is_expected.to redirect_to(about_path)
       end
     end
 
     context 'when signed in' do
       let(:user) { Fabricate(:user) }
 
-      subject do
-        sign_in(user)
-        get :index
-      end
+      before { sign_in(user) }
 
       it 'assigns @body_classes' do
         subject
diff --git a/spec/helpers/instance_helper_spec.rb b/spec/helpers/instance_helper_spec.rb
index bc5950d91..c2e26dbed 100644
--- a/spec/helpers/instance_helper_spec.rb
+++ b/spec/helpers/instance_helper_spec.rb
@@ -15,12 +15,6 @@ describe InstanceHelper do
 
       expect(helper.site_title).to eq 'New site title'
     end
-
-    it 'returns empty string when Setting.site_title is nil' do
-      Setting.site_title = nil
-
-      expect(helper.site_title).to eq 'cb6e6126.ngrok.io'
-    end
   end
 
   describe 'site_hostname' do
diff --git a/spec/lib/activitypub/activity/add_spec.rb b/spec/lib/activitypub/activity/add_spec.rb
new file mode 100644
index 000000000..3ebab4e37
--- /dev/null
+++ b/spec/lib/activitypub/activity/add_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Add do
+  let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') }
+  let(:status) { Fabricate(:status, account: sender) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Add',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: ActivityPub::TagManager.instance.uri_for(status),
+      target: sender.featured_collection_url,
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      subject.perform
+    end
+
+    it 'creates a pin' do
+      expect(sender.pinned?(status)).to be true
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index 51f54a398..62b9db8c2 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -202,7 +202,7 @@ RSpec.describe ActivityPub::Activity::Create do
           attachment: [
             {
               type: 'Document',
-              mime_type: 'image/png',
+              mediaType: 'image/png',
               url: 'http://example.com/attachment.png',
             },
           ],
@@ -217,6 +217,31 @@ RSpec.describe ActivityPub::Activity::Create do
       end
     end
 
+    context 'with media attachments with focal points' do
+      let(:object_json) do
+        {
+          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+          type: 'Note',
+          content: 'Lorem ipsum',
+          attachment: [
+            {
+              type: 'Document',
+              mediaType: 'image/png',
+              url: 'http://example.com/attachment.png',
+              focalPoint: [0.5, -0.7],
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7')
+      end
+    end
+
     context 'with media attachments missing url' do
       let(:object_json) do
         {
@@ -226,7 +251,7 @@ RSpec.describe ActivityPub::Activity::Create do
           attachment: [
             {
               type: 'Document',
-              mime_type: 'image/png',
+              mediaType: 'image/png',
             },
           ],
         }
diff --git a/spec/lib/activitypub/activity/remove_spec.rb b/spec/lib/activitypub/activity/remove_spec.rb
new file mode 100644
index 000000000..4209dfde2
--- /dev/null
+++ b/spec/lib/activitypub/activity/remove_spec.rb
@@ -0,0 +1,30 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Remove do
+  let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') }
+  let(:status) { Fabricate(:status, account: sender) }
+
+  let(:json) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'foo',
+      type: 'Add',
+      actor: ActivityPub::TagManager.instance.uri_for(sender),
+      object: ActivityPub::TagManager.instance.uri_for(status),
+      target: sender.featured_collection_url,
+    }.with_indifferent_access
+  end
+
+  describe '#perform' do
+    subject { described_class.new(json, sender) }
+
+    before do
+      StatusPin.create!(account: sender, status: status)
+      subject.perform
+    end
+
+    it 'removes a pin' do
+      expect(sender.pinned?(status)).to be false
+    end
+  end
+end
diff --git a/spec/lib/activitypub/activity/update_spec.rb b/spec/lib/activitypub/activity/update_spec.rb
index ea308e35c..fbfc585cf 100644
--- a/spec/lib/activitypub/activity/update_spec.rb
+++ b/spec/lib/activitypub/activity/update_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe ActivityPub::Activity::Update do
     stub_request(:get, actor_json[:outbox]).to_return(status: 404)
     stub_request(:get, actor_json[:followers]).to_return(status: 404)
     stub_request(:get, actor_json[:following]).to_return(status: 404)
+    stub_request(:get, actor_json[:featured]).to_return(status: 404)
 
     sender.update!(uri: ActivityPub::TagManager.instance.uri_for(sender))
   end
diff --git a/yarn.lock b/yarn.lock
index cc888a7b9..c97015fd8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -244,6 +244,10 @@ array-equal@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
 
+array-filter@~0.0.0:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
+
 array-find-index@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
@@ -263,6 +267,14 @@ array-includes@^3.0.3:
     define-properties "^1.1.2"
     es-abstract "^1.7.0"
 
+array-map@~0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
+
+array-reduce@~0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b"
+
 array-union@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
@@ -2170,7 +2182,7 @@ double-ended-queue@^2.1.0-0:
   version "2.1.0-0"
   resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
 
-duplexer@^0.1.1:
+duplexer@^0.1.1, duplexer@~0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
 
@@ -2286,7 +2298,7 @@ error-ex@^1.2.0, error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.6.1, es-abstract@^1.7.0:
+es-abstract@^1.4.3, es-abstract@^1.6.1, es-abstract@^1.7.0:
   version "1.10.0"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864"
   dependencies:
@@ -2538,6 +2550,18 @@ event-emitter@~0.3.5:
     d "1"
     es5-ext "~0.10.14"
 
+event-stream@~3.3.0:
+  version "3.3.4"
+  resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
+  dependencies:
+    duplexer "~0.1.1"
+    from "~0"
+    map-stream "~0.1.0"
+    pause-stream "0.0.11"
+    split "0.3"
+    stream-combiner "~0.0.4"
+    through "~2.3.1"
+
 eventemitter3@1.x.x:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
@@ -2872,6 +2896,10 @@ fresh@0.5.2:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
 
+from@~0:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
+
 fs-extra@^0.30.0:
   version "0.30.0"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0"
@@ -4052,6 +4080,10 @@ json-loader@^0.5.4:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
 
+json-parse-better-errors@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz#50183cd1b2d25275de069e9e71b467ac9eab973a"
+
 json-schema-traverse@^0.3.0:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
@@ -4191,6 +4223,15 @@ load-json-file@^2.0.0:
     pify "^2.0.0"
     strip-bom "^3.0.0"
 
+load-json-file@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^4.0.0"
+    pify "^3.0.0"
+    strip-bom "^3.0.0"
+
 loader-runner@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
@@ -4381,6 +4422,10 @@ map-obj@^1.0.0, map-obj@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
 
+map-stream@~0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
+
 mark-loader@^0.1.6:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/mark-loader/-/mark-loader-0.1.6.tgz#0abb477dca7421d70e20128ff6489f5cae8676d5"
@@ -4429,6 +4474,10 @@ memory-fs@^0.4.0, memory-fs@~0.4.1:
     errno "^0.1.3"
     readable-stream "^2.0.1"
 
+memorystream@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
+
 meow@^3.3.0, meow@^3.7.0:
   version "3.7.0"
   resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
@@ -4760,6 +4809,20 @@ normalize-url@^1.4.0:
     query-string "^4.1.0"
     sort-keys "^1.0.0"
 
+npm-run-all@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.2.tgz#90d62d078792d20669139e718621186656cea056"
+  dependencies:
+    ansi-styles "^3.2.0"
+    chalk "^2.1.0"
+    cross-spawn "^5.1.0"
+    memorystream "^0.3.1"
+    minimatch "^3.0.4"
+    ps-tree "^1.1.0"
+    read-pkg "^3.0.0"
+    shell-quote "^1.6.1"
+    string.prototype.padend "^3.0.0"
+
 npm-run-path@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -5032,6 +5095,13 @@ parse-json@^3.0.0:
   dependencies:
     error-ex "^1.3.1"
 
+parse-json@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+  dependencies:
+    error-ex "^1.3.1"
+    json-parse-better-errors "^1.0.1"
+
 parse5@^1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
@@ -5104,6 +5174,18 @@ path-type@^2.0.0:
   dependencies:
     pify "^2.0.0"
 
+path-type@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+  dependencies:
+    pify "^3.0.0"
+
+pause-stream@0.0.11:
+  version "0.0.11"
+  resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
+  dependencies:
+    through "~2.3"
+
 pbkdf2@^3.0.3:
   version "3.0.14"
   resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade"
@@ -5767,6 +5849,12 @@ prr@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
 
+ps-tree@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014"
+  dependencies:
+    event-stream "~3.3.0"
+
 pseudomap@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
@@ -6130,6 +6218,14 @@ read-pkg@^2.0.0:
     normalize-package-data "^2.3.2"
     path-type "^2.0.0"
 
+read-pkg@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
+  dependencies:
+    load-json-file "^4.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^3.0.0"
+
 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"
@@ -6679,6 +6775,15 @@ shebang-regex@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
 
+shell-quote@^1.6.1:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"
+  dependencies:
+    array-filter "~0.0.0"
+    array-map "~0.0.0"
+    array-reduce "~0.0.0"
+    jsonify "~0.0.0"
+
 shellwords@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
@@ -6813,6 +6918,12 @@ spdy@^3.4.1:
     select-hose "^2.0.0"
     spdy-transport "^2.0.18"
 
+split@0.3:
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f"
+  dependencies:
+    through "2"
+
 split@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
@@ -6858,6 +6969,12 @@ stream-browserify@^2.0.1:
     inherits "~2.0.1"
     readable-stream "^2.0.2"
 
+stream-combiner@~0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
+  dependencies:
+    duplexer "~0.1.1"
+
 stream-http@^2.7.2:
   version "2.7.2"
   resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad"
@@ -6894,6 +7011,14 @@ string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^4.0.0"
 
+string.prototype.padend@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0"
+  dependencies:
+    define-properties "^1.1.2"
+    es-abstract "^1.4.3"
+    function-bind "^1.0.2"
+
 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"
@@ -7061,7 +7186,7 @@ throng@^4.0.0:
   dependencies:
     lodash.defaults "^4.0.1"
 
-through@2, through@^2.3.6:
+through@2, through@^2.3.6, through@~2.3, through@~2.3.1:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"