about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.editorconfig12
-rw-r--r--.env.production.sample8
-rw-r--r--.eslintignore30
-rw-r--r--.ruby-version2
-rw-r--r--.travis.yml2
-rw-r--r--Dockerfile2
-rw-r--r--Gemfile40
-rw-r--r--Gemfile.lock208
-rw-r--r--README.md2
-rw-r--r--Vagrantfile8
-rw-r--r--app.json12
-rw-r--r--app/assets/javascripts/components/actions/notifications.jsx12
-rw-r--r--app/assets/javascripts/components/components/account.jsx2
-rw-r--r--app/assets/javascripts/components/components/avatar.jsx135
-rw-r--r--app/assets/javascripts/components/components/status.jsx2
-rw-r--r--app/assets/javascripts/components/components/status_content.jsx13
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx6
-rw-r--r--app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx22
-rw-r--r--app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/navigation_bar.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/reply_indicator.jsx2
-rw-r--r--app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx2
-rw-r--r--app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx12
-rw-r--r--app/assets/javascripts/components/features/notifications/components/notification.jsx8
-rw-r--r--app/assets/javascripts/components/features/status/components/detailed_status.jsx2
-rw-r--r--app/assets/javascripts/components/locales/fr.jsx2
-rw-r--r--app/assets/javascripts/components/locales/index.jsx6
-rw-r--r--app/assets/javascripts/components/locales/pt.jsx71
-rw-r--r--app/assets/javascripts/components/locales/ru.jsx68
-rw-r--r--app/assets/stylesheets/accounts.scss15
-rw-r--r--app/assets/stylesheets/components.scss146
-rw-r--r--app/controllers/about_controller.rb29
-rw-r--r--app/controllers/accounts_controller.rb6
-rw-r--r--app/controllers/admin/accounts_controller.rb93
-rw-r--r--app/controllers/admin/base_controller.rb9
-rw-r--r--app/controllers/admin/domain_blocks_controller.rb42
-rw-r--r--app/controllers/admin/pubsubhubbub_controller.rb12
-rw-r--r--app/controllers/admin/reports_controller.rb81
-rw-r--r--app/controllers/admin/settings_controller.rb46
-rw-r--r--app/controllers/api/v1/accounts_controller.rb15
-rw-r--r--app/controllers/api/v1/notifications_controller.rb10
-rw-r--r--app/controllers/api_controller.rb1
-rw-r--r--app/controllers/concerns/localized.rb4
-rw-r--r--app/controllers/remote_follow_controller.rb2
-rw-r--r--app/controllers/settings/exports_controller.rb2
-rw-r--r--app/controllers/xrd_controller.rb2
-rw-r--r--app/helpers/about_helper.rb4
-rw-r--r--app/helpers/accounts_helper.rb12
-rw-r--r--app/helpers/admin/domain_blocks_helper.rb4
-rw-r--r--app/helpers/admin/pubsubhubbub_helper.rb4
-rw-r--r--app/helpers/atom_builder_helper.rb2
-rw-r--r--app/helpers/authorize_follow_helper.rb4
-rw-r--r--app/helpers/settings_helper.rb4
-rw-r--r--app/helpers/tags_helper.rb4
-rw-r--r--app/helpers/xrd_helper.rb4
-rw-r--r--app/lib/atom_serializer.rb4
-rw-r--r--app/lib/feed_manager.rb2
-rw-r--r--app/lib/formatter.rb1
-rw-r--r--app/models/account.rb52
-rw-r--r--app/models/notification.rb27
-rw-r--r--app/models/status.rb2
-rw-r--r--app/models/tag.rb4
-rw-r--r--app/presenters/instance_presenter.rb28
-rw-r--r--app/services/fetch_remote_account_service.rb13
-rw-r--r--app/views/about/_registration.html.haml30
-rw-r--r--app/views/about/more.html.haml28
-rw-r--r--app/views/about/show.html.haml (renamed from app/views/about/index.html.haml)28
-rw-r--r--app/views/accounts/followers.html.haml2
-rw-r--r--app/views/accounts/following.html.haml2
-rw-r--r--app/views/accounts/show.html.haml2
-rw-r--r--app/views/admin/accounts/index.html.haml2
-rw-r--r--app/views/admin/domain_blocks/index.html.haml2
-rw-r--r--app/views/admin/pubsubhubbub/index.html.haml2
-rw-r--r--app/views/admin/reports/index.html.haml2
-rw-r--r--app/views/api/v1/accounts/show.rabl11
-rw-r--r--app/views/kaminari/_next_page.html.haml9
-rw-r--r--app/views/kaminari/_paginator.html.haml16
-rw-r--r--app/views/kaminari/_prev_page.html.haml9
-rw-r--r--app/views/shared/_landing_strip.html.haml5
-rw-r--r--app/views/stream_entries/_status.html.haml2
-rw-r--r--app/views/tags/show.html.haml2
-rw-r--r--app/views/user_mailer/confirmation_instructions.fr.html.erb2
-rw-r--r--app/views/user_mailer/confirmation_instructions.fr.text.erb2
-rw-r--r--app/workers/import_worker.rb4
-rw-r--r--config/application.rb4
-rw-r--r--config/database.yml1
-rw-r--r--config/environments/production.rb8
-rw-r--r--config/i18n-tasks.yml2
-rw-r--r--config/initializers/devise.rb3
-rw-r--r--config/initializers/httplog.rb8
-rw-r--r--config/initializers/kaminari_config.rb7
-rw-r--r--config/initializers/pagination.rb0
-rw-r--r--config/locales/de.yml2
-rw-r--r--config/locales/devise.fr.yml2
-rw-r--r--config/locales/devise.ru.yml61
-rw-r--r--config/locales/doorkeeper.fr.yml8
-rw-r--r--config/locales/doorkeeper.ru.yml113
-rw-r--r--config/locales/en.yml3
-rw-r--r--config/locales/eo.yml2
-rw-r--r--config/locales/es.yml2
-rw-r--r--config/locales/fi.yml2
-rw-r--r--config/locales/fr.yml38
-rw-r--r--config/locales/hu.yml2
-rw-r--r--config/locales/no.yml2
-rw-r--r--config/locales/pt.yml2
-rw-r--r--config/locales/ru.yml163
-rw-r--r--config/locales/simple_form.ru.yml46
-rw-r--r--config/locales/uk.yml2
-rw-r--r--config/locales/zh-CN.yml2
-rw-r--r--config/routes.rb8
-rw-r--r--config/settings.yml7
-rw-r--r--docker-compose.yml11
-rw-r--r--docs/Running-Mastodon/Administration-guide.md8
-rw-r--r--docs/Running-Mastodon/Production-guide.md16
-rw-r--r--docs/Using-Mastodon/Apps.md1
-rw-r--r--docs/Using-Mastodon/List-of-Mastodon-instances.md2
-rw-r--r--docs/Using-the-API/API.md29
-rw-r--r--docs/Using-the-API/OAuth-details.md2
-rw-r--r--docs/Using-the-API/Testing-with-cURL.md4
-rw-r--r--docs/Using-the-API/Tips-for-app-developers.md2
-rw-r--r--lib/tasks/mastodon.rake24
-rw-r--r--package.json5
-rw-r--r--scalingo.json12
-rw-r--r--spec/controllers/about_controller_spec.rb11
-rw-r--r--spec/controllers/admin/reports_controller_spec.rb14
-rw-r--r--spec/controllers/admin/settings_controller_spec.rb14
-rw-r--r--spec/controllers/api/v1/accounts_controller_spec.rb39
-rw-r--r--spec/controllers/api/v1/notifications_controller_spec.rb62
-rw-r--r--spec/controllers/auth/registrations_controller_spec.rb2
-rw-r--r--spec/controllers/xrd_controller_spec.rb2
-rw-r--r--spec/fixtures/files/avatar.gifbin0 -> 85810 bytes
-rw-r--r--spec/helpers/about_helper_spec.rb5
-rw-r--r--spec/helpers/accounts_helper_spec.rb5
-rw-r--r--spec/helpers/admin/domain_blocks_helper_spec.rb5
-rw-r--r--spec/helpers/admin/pubsubhubbub_helper_spec.rb5
-rw-r--r--spec/helpers/application_helper_spec.rb16
-rw-r--r--spec/helpers/authorize_follow_helper_spec.rb5
-rw-r--r--spec/helpers/stream_entries_helper_spec.rb12
-rw-r--r--spec/helpers/tags_helper_spec.rb5
-rw-r--r--spec/helpers/xrd_helper_spec.rb5
-rw-r--r--spec/javascript/components/avatar.test.jsx12
-rw-r--r--spec/models/account_spec.rb99
-rw-r--r--spec/models/tag_spec.rb11
-rw-r--r--spec/presenters/instance_presenter_spec.rb74
-rw-r--r--spec/requests/catch_all_route_request_spec.rb21
-rw-r--r--streaming/index.js15
-rw-r--r--yarn.lock486
148 files changed, 2297 insertions, 793 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..5f8702cf8
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+# EditorConfig is awesome: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 2
diff --git a/.env.production.sample b/.env.production.sample
index d7c04e235..97bba5e3f 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -35,6 +35,10 @@ SMTP_PORT=587
 SMTP_LOGIN=
 SMTP_PASSWORD=
 SMTP_FROM_ADDRESS=notifications@example.com
+#SMTP_AUTH_METHOD=plain
+#SMTP_OPENSSL_VERIFY_MODE=peer
+#SMTP_ENABLE_STARTTLS_AUTO=true
+
 
 # Optional asset host for multi-server setups
 # CDN_HOST=assets.example.com
@@ -63,3 +67,7 @@ SMTP_FROM_ADDRESS=notifications@example.com
 
 # Streaming API integration
 # STREAMING_API_BASE_URL=
+
+# Advanced settings
+# If you need to use pgBouncer, you need to disable prepared statements:
+# PREPARED_STATEMENTS=false
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 000000000..6d540c413
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,30 @@
+# See https://help.github.com/articles/ignoring-files for more about ignoring files.
+#
+# If you find yourself ignoring temporary files generated by your text editor
+# or operating system, you probably want to add a global ignore instead:
+#   git config --global core.excludesfile '~/.gitignore_global'
+
+# Ignore bundler config.
+/.bundle
+
+# Ignore the default SQLite database.
+/db/*.sqlite3
+/db/*.sqlite3-journal
+
+# Ignore all logfiles and tempfiles.
+/log/*
+!/log/.keep
+/tmp
+coverage
+public/system
+public/assets
+.env
+.env.production
+node_modules/
+neo4j/
+
+# Ignore Vagrant files
+.vagrant/
+
+# Ignore Capistrano customizations
+config/deploy/*
diff --git a/.ruby-version b/.ruby-version
index 2bf1c1ccf..005119baa 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.3.1
+2.4.1
diff --git a/.travis.yml b/.travis.yml
index b1b0c2bcd..a9824ccf7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,7 +16,7 @@ addons:
   postgresql: 9.4
 
 rvm:
-  - 2.3.1
+  - 2.4.1
 
 services:
   - redis-server
diff --git a/Dockerfile b/Dockerfile
index 57a8f34e9..a05525b33 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM ruby:2.3.1-alpine
+FROM ruby:2.4.1-alpine
 
 LABEL maintainer="https://github.com/tootsuite/mastodon" \
       description="A GNU Social-compatible microblogging server"
diff --git a/Gemfile b/Gemfile
index 65bd5eb49..9a1792623 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 source 'https://rubygems.org'
-ruby '2.3.1'
+ruby '2.4.1'
 
 gem 'rails', '~> 5.0.2'
 gem 'sass-rails', '~> 5.0'
@@ -21,36 +21,37 @@ gem 'paperclip', '~> 5.1'
 gem 'paperclip-av-transcoder'
 gem 'aws-sdk', '>= 2.0'
 
-gem 'http'
-gem 'httplog'
 gem 'addressable'
-gem 'nokogiri'
-gem 'link_header'
-gem 'ostatus2'
-gem 'goldfinger'
 gem 'devise'
 gem 'devise-two-factor'
 gem 'doorkeeper'
-gem 'rabl'
-gem 'rqrcode'
-gem 'twitter-text'
-gem 'ox'
-gem 'oj'
-gem 'hiredis'
-gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
 gem 'fast_blank'
+gem 'goldfinger'
+gem 'hiredis'
 gem 'htmlentities'
-gem 'simple_form'
-gem 'will_paginate'
+gem 'http'
+gem 'http_accept_language'
+gem 'httplog'
+gem 'kaminari'
+gem 'link_header'
+gem 'nokogiri'
+gem 'oj'
+gem 'ostatus2'
+gem 'ox'
+gem 'rabl'
 gem 'rack-attack'
 gem 'rack-cors', require: 'rack/cors'
+gem 'rack-timeout'
+gem 'rails-settings-cached'
+gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
+gem 'rqrcode'
+gem 'ruby-oembed', require: 'oembed'
 gem 'sidekiq'
 gem 'sidekiq-unique-jobs'
-gem 'rails-settings-cached'
 gem 'simple-navigation'
+gem 'simple_form'
 gem 'statsd-instrument'
-gem 'ruby-oembed', require: 'oembed'
-gem 'rack-timeout'
+gem 'twitter-text'
 gem 'tzinfo-data'
 
 gem 'react-rails'
@@ -67,6 +68,7 @@ end
 
 group :test do
   gem 'faker'
+  gem 'rails-controller-testing'
   gem 'rspec-sidekiq'
   gem 'simplecov', require: false
   gem 'webmock'
diff --git a/Gemfile.lock b/Gemfile.lock
index f2a199931..f1bc9880e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -24,7 +24,7 @@ GEM
       erubis (~> 2.7.0)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.0.3)
-    active_record_query_trace (1.5.3)
+    active_record_query_trace (1.5.4)
     activejob (5.0.2)
       activesupport (= 5.0.2)
       globalid (>= 0.3.6)
@@ -39,7 +39,7 @@ GEM
       i18n (~> 0.7)
       minitest (~> 5.1)
       tzinfo (~> 1.1)
-    addressable (2.5.0)
+    addressable (2.5.1)
       public_suffix (~> 2.0, >= 2.0.2)
     airbrussh (1.1.2)
       sshkit (>= 1.6.1, != 1.7.0)
@@ -47,17 +47,17 @@ GEM
     ast (2.3.0)
     attr_encrypted (3.0.3)
       encryptor (~> 3.0.0)
-    autoprefixer-rails (6.5.0.2)
+    autoprefixer-rails (6.7.7.1)
       execjs
     av (0.9.0)
       cocaine (~> 0.5.3)
-    aws-sdk (2.6.28)
-      aws-sdk-resources (= 2.6.28)
-    aws-sdk-core (2.6.28)
+    aws-sdk (2.9.6)
+      aws-sdk-resources (= 2.9.6)
+    aws-sdk-core (2.9.6)
       aws-sigv4 (~> 1.0)
       jmespath (~> 1.0)
-    aws-sdk-resources (2.6.28)
-      aws-sdk-core (= 2.6.28)
+    aws-sdk-resources (2.9.6)
+      aws-sdk-core (= 2.9.6)
     aws-sigv4 (1.0.0)
     babel-source (5.8.35)
     babel-transpiler (0.7.0)
@@ -78,12 +78,11 @@ GEM
       railties (>= 4.0.0, < 5.1)
       sprockets (>= 3.6.0)
     builder (3.2.3)
-    bullet (5.3.0)
+    bullet (5.5.1)
       activesupport (>= 3.0.0)
       uniform_notifier (~> 1.10.0)
-    capistrano (3.7.2)
+    capistrano (3.8.0)
       airbrussh (>= 1.0.0)
-      capistrano-harrow
       i18n
       rake (>= 10.0.0)
       sshkit (>= 1.9.0)
@@ -92,8 +91,7 @@ GEM
       sshkit (~> 1.2)
     capistrano-faster-assets (1.0.2)
       capistrano (>= 3.1)
-    capistrano-harrow (0.5.3)
-    capistrano-rails (1.2.2)
+    capistrano-rails (1.2.3)
       capistrano (~> 3.1)
       capistrano-bundler (~> 1.1)
     capistrano-rbenv (2.1.0)
@@ -119,7 +117,7 @@ GEM
     crack (0.4.3)
       safe_yaml (~> 1.0.0)
     debug_inspector (0.0.2)
-    devise (4.2.0)
+    devise (4.2.1)
       bcrypt (~> 3.0)
       orm_adapter (~> 0.1)
       railties (>= 4.1.0, < 5.1)
@@ -131,16 +129,16 @@ GEM
       devise (~> 4.0)
       railties
       rotp (~> 2.0)
-    diff-lcs (1.2.5)
+    diff-lcs (1.3)
     docile (1.1.5)
-    domain_name (0.5.20161129)
+    domain_name (0.5.20170404)
       unf (>= 0.0.5, < 1.0.0)
-    doorkeeper (4.2.0)
+    doorkeeper (4.2.5)
       railties (>= 4.2)
-    dotenv (2.1.1)
-    dotenv-rails (2.1.1)
-      dotenv (= 2.1.1)
-      railties (>= 4.0, < 5.1)
+    dotenv (2.2.0)
+    dotenv-rails (2.2.0)
+      dotenv (= 2.2.0)
+      railties (>= 3.2, < 5.1)
     easy_translate (0.5.0)
       json
       thread
@@ -148,14 +146,14 @@ GEM
     encryptor (3.0.0)
     erubis (2.7.0)
     execjs (2.7.0)
-    fabrication (2.15.2)
-    faker (1.6.6)
+    fabrication (2.16.1)
+    faker (1.7.3)
       i18n (~> 0.5)
     fast_blank (1.0.0)
-    font-awesome-rails (4.6.3.1)
+    font-awesome-rails (4.7.0.1)
       railties (>= 3.2, < 5.1)
-    fuubar (2.1.1)
-      rspec (~> 3.0)
+    fuubar (2.2.0)
+      rspec-core (~> 3.0)
       ruby-progressbar (~> 1.4)
     globalid (0.3.7)
       activesupport (>= 4.1.0)
@@ -163,20 +161,20 @@ GEM
       addressable (~> 2.4)
       http (~> 2.0)
       nokogiri (~> 1.6)
-    hamlit (2.7.2)
-      temple (~> 0.7.6)
+    hamlit (2.8.1)
+      temple (>= 0.8.0)
       thor
       tilt
-    hamlit-rails (0.1.0)
+    hamlit-rails (0.2.0)
       actionpack (>= 4.0.1)
       activesupport (>= 4.0.1)
       hamlit (>= 1.2.0)
       railties (>= 4.0.1)
-    hashdiff (0.3.0)
+    hashdiff (0.3.2)
     highline (1.7.8)
     hiredis (0.6.1)
     htmlentities (4.3.4)
-    http (2.1.0)
+    http (2.2.1)
       addressable (~> 2.3)
       http-cookie (~> 1.0)
       http-form_data (~> 1.0.1)
@@ -184,11 +182,12 @@ GEM
     http-cookie (1.0.3)
       domain_name (~> 0.5)
     http-form_data (1.0.1)
+    http_accept_language (2.1.0)
     http_parser.rb (0.6.0)
-    httplog (0.3.2)
+    httplog (0.99.2)
       colorize
     i18n (0.8.1)
-    i18n-tasks (0.9.6)
+    i18n-tasks (0.9.13)
       activesupport (>= 4.0.2)
       ast (>= 2.1.0)
       easy_translate (>= 0.5.0)
@@ -196,19 +195,31 @@ GEM
       highline (>= 1.7.3)
       i18n
       parser (>= 2.2.3.0)
-      term-ansicolor (>= 1.3.2)
+      rainbow (~> 2.2)
       terminal-table (>= 1.5.1)
     jmespath (1.3.1)
-    jquery-rails (4.1.1)
+    jquery-rails (4.3.1)
       rails-dom-testing (>= 1, < 3)
       railties (>= 4.2.0)
       thor (>= 0.14, < 2.0)
-    json (1.8.3)
+    json (2.0.3)
+    kaminari (1.0.1)
+      activesupport (>= 4.1.0)
+      kaminari-actionview (= 1.0.1)
+      kaminari-activerecord (= 1.0.1)
+      kaminari-core (= 1.0.1)
+    kaminari-actionview (1.0.1)
+      actionview
+      kaminari-core (= 1.0.1)
+    kaminari-activerecord (1.0.1)
+      activerecord
+      kaminari-core (= 1.0.1)
+    kaminari-core (1.0.1)
     launchy (2.4.3)
       addressable (~> 2.3)
     letter_opener (1.4.1)
       launchy (~> 2.2)
-    letter_opener_web (1.3.0)
+    letter_opener_web (1.3.1)
       actionmailer (>= 3.2)
       letter_opener (~> 1.0)
       railties (>= 3.2)
@@ -230,11 +241,11 @@ GEM
     minitest (5.10.1)
     net-scp (1.2.1)
       net-ssh (>= 2.6.5)
-    net-ssh (4.0.1)
+    net-ssh (4.1.0)
     nio4r (2.0.0)
     nokogiri (1.7.1)
       mini_portile2 (~> 2.1.0)
-    oj (2.17.3)
+    oj (2.18.5)
     orm_adapter (0.5.0)
     ostatus2 (1.0.2)
       addressable (~> 2.4)
@@ -250,26 +261,26 @@ GEM
     paperclip-av-transcoder (0.6.4)
       av (~> 0.9.0)
       paperclip (>= 2.5.2)
-    parser (2.3.1.2)
+    parser (2.4.0.0)
       ast (~> 2.2)
-    pg (0.18.4)
-    pghero (1.6.2)
+    pg (0.20.0)
+    pghero (1.6.4)
       activerecord
     powerpack (0.1.1)
     pry (0.10.4)
       coderay (~> 1.1.0)
       method_source (~> 0.8.1)
       slop (~> 3.4)
-    pry-rails (0.3.4)
-      pry (>= 0.9.10)
-    public_suffix (2.0.4)
-    puma (3.6.0)
+    pry-rails (0.3.6)
+      pry (>= 0.10.4)
+    public_suffix (2.0.5)
+    puma (3.8.2)
     rabl (0.13.1)
       activesupport (>= 2.3.14)
     rack (2.0.1)
     rack-attack (5.0.1)
       rack
-    rack-cors (0.4.0)
+    rack-cors (0.4.1)
     rack-protection (1.5.3)
       rack
     rack-test (0.6.3)
@@ -287,6 +298,10 @@ GEM
       bundler (>= 1.3.0, < 2.0)
       railties (= 5.0.2)
       sprockets-rails (>= 2.0.0)
+    rails-controller-testing (1.0.1)
+      actionpack (~> 5.x)
+      actionview (~> 5.x)
+      activesupport (~> 5.x)
     rails-dom-testing (2.0.2)
       activesupport (>= 4.2.0, < 6.0)
       nokogiri (~> 1.6)
@@ -305,42 +320,37 @@ GEM
       method_source
       rake (>= 0.8.7)
       thor (>= 0.18.1, < 2.0)
-    rainbow (2.1.0)
+    rainbow (2.2.1)
     rake (12.0.0)
-    react-rails (1.10.0)
+    react-rails (1.11.0)
       babel-transpiler (>= 0.7.0)
-      coffee-script-source (~> 1.8)
       connection_pool
       execjs
       railties (>= 3.2)
       tilt
-    redis (3.3.2)
-    redis-actionpack (5.0.0)
-      actionpack (>= 4.0.0, < 6)
-      redis-rack (~> 2.0.0.pre)
-      redis-store (~> 1.2.0.pre)
-    redis-activesupport (5.0.1)
+    redis (3.3.3)
+    redis-actionpack (5.0.1)
+      actionpack (>= 4.0, < 6)
+      redis-rack (>= 1, < 3)
+      redis-store (>= 1.1.0, < 1.4.0)
+    redis-activesupport (5.0.2)
       activesupport (>= 3, < 6)
-      redis-store (~> 1.2.0)
-    redis-rack (2.0.0)
-      rack (~> 2.0)
-      redis-store (~> 1.2.0)
-    redis-rails (5.0.1)
-      redis-actionpack (~> 5.0.0)
-      redis-activesupport (~> 5.0.0)
-      redis-store (~> 1.2.0)
-    redis-store (1.2.0)
+      redis-store (~> 1.3.0)
+    redis-rack (2.0.1)
+      rack (>= 2.0, < 3)
+      redis-store (>= 1.2, < 1.4)
+    redis-rails (5.0.2)
+      redis-actionpack (>= 5.0, < 6)
+      redis-activesupport (>= 5.0, < 6)
+      redis-store (>= 1.2, < 2)
+    redis-store (1.3.0)
       redis (>= 2.2)
     responders (2.3.0)
       railties (>= 4.2.0, < 5.1)
     rotp (2.1.2)
     rqrcode (0.10.1)
       chunky_png (~> 1.0)
-    rspec (3.5.0)
-      rspec-core (~> 3.5.0)
-      rspec-expectations (~> 3.5.0)
-      rspec-mocks (~> 3.5.0)
-    rspec-core (3.5.2)
+    rspec-core (3.5.4)
       rspec-support (~> 3.5.0)
     rspec-expectations (3.5.0)
       diff-lcs (>= 1.2.0, < 2.0)
@@ -348,7 +358,7 @@ GEM
     rspec-mocks (3.5.0)
       diff-lcs (>= 1.2.0, < 2.0)
       rspec-support (~> 3.5.0)
-    rspec-rails (3.5.1)
+    rspec-rails (3.5.2)
       actionpack (>= 3.0)
       activesupport (>= 3.0)
       railties (>= 3.0)
@@ -356,40 +366,40 @@ GEM
       rspec-expectations (~> 3.5.0)
       rspec-mocks (~> 3.5.0)
       rspec-support (~> 3.5.0)
-    rspec-sidekiq (2.2.0)
-      rspec (~> 3.0, >= 3.0.0)
+    rspec-sidekiq (3.0.0)
+      rspec-core (~> 3.0, >= 3.0.0)
       sidekiq (>= 2.4.0)
     rspec-support (3.5.0)
-    rubocop (0.42.0)
-      parser (>= 2.3.1.1, < 3.0)
+    rubocop (0.48.1)
+      parser (>= 2.3.3.1, < 3.0)
       powerpack (~> 0.1)
       rainbow (>= 1.99.1, < 3.0)
       ruby-progressbar (~> 1.7)
       unicode-display_width (~> 1.0, >= 1.0.1)
-    ruby-oembed (0.10.1)
+    ruby-oembed (0.12.0)
     ruby-progressbar (1.8.1)
     safe_yaml (1.0.4)
-    sass (3.4.22)
+    sass (3.4.23)
     sass-rails (5.0.6)
       railties (>= 4.0.0, < 6)
       sass (~> 3.1)
       sprockets (>= 2.8, < 4.0)
       sprockets-rails (>= 2.0, < 4.0)
       tilt (>= 1.1, < 3)
-    sidekiq (4.2.7)
+    sidekiq (4.2.10)
       concurrent-ruby (~> 1.0)
       connection_pool (~> 2.2, >= 2.2.0)
       rack-protection (>= 1.5.0)
       redis (~> 3.2, >= 3.2.1)
-    sidekiq-unique-jobs (4.0.18)
-      sidekiq (>= 2.6)
+    sidekiq-unique-jobs (5.0.0)
+      sidekiq (>= 4.0)
       thor
-    simple-navigation (4.0.3)
+    simple-navigation (4.0.5)
       activesupport (>= 2.3.2)
-    simple_form (3.2.1)
+    simple_form (3.4.0)
       actionpack (> 4, < 5.1)
       activemodel (> 4, < 5.1)
-    simplecov (0.12.0)
+    simplecov (0.14.1)
       docile (~> 1.1.0)
       json (>= 1.8, < 3)
       simplecov-html (~> 0.10.0)
@@ -402,43 +412,39 @@ GEM
       actionpack (>= 4.0)
       activesupport (>= 4.0)
       sprockets (>= 3.0.0)
-    sshkit (1.11.5)
+    sshkit (1.13.1)
       net-scp (>= 1.1.2)
       net-ssh (>= 2.8.0)
     statsd-instrument (2.1.2)
-    temple (0.7.7)
-    term-ansicolor (1.4.0)
-      tins (~> 1.0)
-    terminal-table (1.7.0)
-      unicode-display_width (~> 1.1)
+    temple (0.8.0)
+    terminal-table (1.7.3)
+      unicode-display_width (~> 1.1.1)
     thor (0.19.4)
     thread (0.2.2)
     thread_safe (0.3.6)
-    tilt (2.0.6)
-    tins (1.12.0)
+    tilt (2.0.7)
     twitter-text (1.14.5)
       unf (~> 0.1.0)
-    tzinfo (1.2.2)
+    tzinfo (1.2.3)
       thread_safe (~> 0.1)
     tzinfo-data (1.2017.2)
       tzinfo (>= 1.0.0)
-    uglifier (3.0.1)
+    uglifier (3.2.0)
       execjs (>= 0.3.0, < 3)
     unf (0.1.4)
       unf_ext
     unf_ext (0.0.7.2)
-    unicode-display_width (1.1.0)
+    unicode-display_width (1.1.3)
     uniform_notifier (1.10.0)
-    warden (1.2.6)
+    warden (1.2.7)
       rack (>= 1.0)
-    webmock (2.1.0)
+    webmock (2.3.2)
       addressable (>= 2.3.6)
       crack (>= 0.3.2)
       hashdiff
     websocket-driver (0.6.5)
       websocket-extensions (>= 0.1.0)
     websocket-extensions (0.1.2)
-    will_paginate (3.1.0)
 
 PLATFORMS
   ruby
@@ -473,9 +479,11 @@ DEPENDENCIES
   hiredis
   htmlentities
   http
+  http_accept_language
   httplog
   i18n-tasks (~> 0.9.6)
   jquery-rails
+  kaminari
   letter_opener
   letter_opener_web
   link_header
@@ -495,6 +503,7 @@ DEPENDENCIES
   rack-cors
   rack-timeout
   rails (~> 5.0.2)
+  rails-controller-testing
   rails-settings-cached
   rails_12factor
   react-rails
@@ -516,10 +525,9 @@ DEPENDENCIES
   tzinfo-data
   uglifier (>= 1.3.0)
   webmock
-  will_paginate
 
 RUBY VERSION
-   ruby 2.3.1p112
+   ruby 2.4.1p111
 
 BUNDLED WITH
-   1.14.3
+   1.14.6
diff --git a/README.md b/README.md
index fa944a901..41990ff70 100644
--- a/README.md
+++ b/README.md
@@ -67,7 +67,7 @@ Consult the example configuration file, `.env.production.sample` for the full li
 
 [![](https://images.microbadger.com/badges/version/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/image/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own image badge on microbadger.com")
 
-The project now includes a `Dockerfile` and a `docker-compose.yml`. You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can:
+The project now includes a `Dockerfile` and a `docker-compose.yml` file (which requires at least docker-compose version `1.10.0`). You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can:
 
     docker-compose build
 
diff --git a/Vagrantfile b/Vagrantfile
index cd7f74473..90f604640 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -46,12 +46,12 @@ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
 export PATH="$HOME/.rbenv/bin::$PATH"
 eval "$(rbenv init -)"
 
-echo "Compiling Ruby 2.3.1: warning, this takes a while!!!"
-rbenv install 2.3.1
-rbenv global 2.3.1
-
 cd /vagrant
 
+echo "Compiling Ruby $(cat .ruby-version): warning, this takes a while!!!"
+rbenv install $(cat .ruby-version)
+rbenv global $(cat .ruby-version)
+
 # Configure database
 sudo -u postgres createuser -U postgres vagrant -s
 sudo -u postgres createdb -U postgres mastodon_development
diff --git a/app.json b/app.json
index 29c1f9f9c..6c4294c79 100644
--- a/app.json
+++ b/app.json
@@ -79,6 +79,18 @@
     "SMTP_FROM_ADDRESS": {
       "description": "Address to send emails from",
       "required": false
+    },
+    "SMTP_AUTH_METHOD": {
+      "description": "Authentication method to use with SMTP server. Default is 'plain'.",
+      "required": false
+    },
+    "SMTP_OPENSSL_VERIFY_MODE": {
+      "description": "SMTP server certificate verification mode. Defaults is 'peer'.",
+      "required": false
+    },
+    "SMTP_ENABLE_STARTTLS_AUTO": {
+      "description": "Enable STARTTLS if SMTP server supports it? Default is true.",
+      "required": false
     }
   },
   "buildpacks": [
diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx
index 980b7d63e..11e814e1f 100644
--- a/app/assets/javascripts/components/actions/notifications.jsx
+++ b/app/assets/javascripts/components/actions/notifications.jsx
@@ -61,6 +61,8 @@ export function refreshNotifications() {
       params.since_id = ids.first().get('id');
     }
 
+    params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
+
     api(getState).get('/api/v1/notifications', { params }).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
 
@@ -105,11 +107,11 @@ export function expandNotifications() {
 
     dispatch(expandNotificationsRequest());
 
-    api(getState).get(url, {
-      params: {
-        limit: 5
-      }
-    }).then(response => {
+    const params = {};
+
+    params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
+
+    api(getState).get(url, params).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
 
       dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx
index 7a1c9f5ce..782cf382d 100644
--- a/app/assets/javascripts/components/components/account.jsx
+++ b/app/assets/javascripts/components/components/account.jsx
@@ -65,7 +65,7 @@ const Account = React.createClass({
       <div className='account'>
         <div style={{ display: 'flex' }}>
           <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
-            <div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={36} /></div>
+            <div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={36} /></div>
             <DisplayName account={account} />
           </Permalink>
 
diff --git a/app/assets/javascripts/components/components/avatar.jsx b/app/assets/javascripts/components/components/avatar.jsx
index 0237a1904..673b1a247 100644
--- a/app/assets/javascripts/components/components/avatar.jsx
+++ b/app/assets/javascripts/components/components/avatar.jsx
@@ -1,103 +1,18 @@
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 
-// From: http://stackoverflow.com/a/18320662
-const resample = (canvas, width, height, resize_canvas) => {
-  let width_source  = canvas.width;
-  let height_source = canvas.height;
-  width  = Math.round(width);
-  height = Math.round(height);
-
-  let ratio_w      = width_source / width;
-  let ratio_h      = height_source / height;
-  let ratio_w_half = Math.ceil(ratio_w / 2);
-  let ratio_h_half = Math.ceil(ratio_h / 2);
-
-  let ctx   = canvas.getContext("2d");
-  let img   = ctx.getImageData(0, 0, width_source, height_source);
-  let img2  = ctx.createImageData(width, height);
-  let data  = img.data;
-  let data2 = img2.data;
-
-  for (let j = 0; j < height; j++) {
-    for (let i = 0; i < width; i++) {
-      let x2            = (i + j * width) * 4;
-      let weight        = 0;
-      let weights       = 0;
-      let weights_alpha = 0;
-      let gx_r          = 0;
-      let gx_g          = 0;
-      let gx_b          = 0;
-      let gx_a          = 0;
-      let center_y      = (j + 0.5) * ratio_h;
-      let yy_start      = Math.floor(j * ratio_h);
-      let yy_stop       = Math.ceil((j + 1) * ratio_h);
-
-      for (let yy = yy_start; yy < yy_stop; yy++) {
-        let dy       = Math.abs(center_y - (yy + 0.5)) / ratio_h_half;
-        let center_x = (i + 0.5) * ratio_w;
-        let w0       = dy * dy; //pre-calc part of w
-        let xx_start = Math.floor(i * ratio_w);
-        let xx_stop  = Math.ceil((i + 1) * ratio_w);
-
-        for (let xx = xx_start; xx < xx_stop; xx++) {
-          let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half;
-          let w  = Math.sqrt(w0 + dx * dx);
-
-          if (w >= 1) {
-            // pixel too far
-            continue;
-          }
-
-          // hermite filter
-          weight    = 2 * w * w * w - 3 * w * w + 1;
-          let pos_x = 4 * (xx + yy * width_source);
-
-          // alpha
-          gx_a          += weight * data[pos_x + 3];
-          weights_alpha += weight;
-
-          // colors
-          if (data[pos_x + 3] < 255)
-            weight = weight * data[pos_x + 3] / 250;
-
-          gx_r    += weight * data[pos_x];
-          gx_g    += weight * data[pos_x + 1];
-          gx_b    += weight * data[pos_x + 2];
-          weights += weight;
-        }
-      }
-
-      data2[x2]     = gx_r / weights;
-      data2[x2 + 1] = gx_g / weights;
-      data2[x2 + 2] = gx_b / weights;
-      data2[x2 + 3] = gx_a / weights_alpha;
-    }
-  }
-
-  // clear and resize canvas
-  if (resize_canvas === true) {
-    canvas.width  = width;
-    canvas.height = height;
-  } else {
-    ctx.clearRect(0, 0, width_source, height_source);
-  }
-
-  // draw
-  ctx.putImageData(img2, 0, 0);
-};
-
 const Avatar = React.createClass({
 
   propTypes: {
     src: React.PropTypes.string.isRequired,
+    staticSrc: React.PropTypes.string,
     size: React.PropTypes.number.isRequired,
     style: React.PropTypes.object,
-    animated: React.PropTypes.bool
+    animate: React.PropTypes.bool
   },
 
   getDefaultProps () {
     return {
-      animated: true
+      animate: false
     };
   },
 
@@ -117,38 +32,30 @@ const Avatar = React.createClass({
     this.setState({ hovering: false });
   },
 
-  handleLoad () {
-    this.canvas.width  = this.image.naturalWidth;
-    this.canvas.height = this.image.naturalHeight;
-    this.canvas.getContext('2d').drawImage(this.image, 0, 0);
-
-    resample(this.canvas, this.props.size * window.devicePixelRatio, this.props.size * window.devicePixelRatio, true);
-  },
-
-  setImageRef (c) {
-    this.image = c;
-  },
-
-  setCanvasRef (c) {
-    this.canvas = c;
-  },
-
   render () {
+    const { src, size, staticSrc, animate } = this.props;
     const { hovering } = this.state;
 
-    if (this.props.animated) {
-      return (
-        <div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}>
-          <img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ borderRadius: '4px' }} />
-        </div>
-      );
+    const style = {
+      ...this.props.style,
+      width: `${size}px`,
+      height: `${size}px`,
+      backgroundSize: `${size}px ${size}px`
+    };
+
+    if (hovering || animate) {
+      style.backgroundImage = `url(${src})`;
+    } else {
+      style.backgroundImage = `url(${staticSrc})`;
     }
 
     return (
-      <div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}>
-        <img ref={this.setImageRef} onLoad={this.handleLoad} src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ position: 'absolute', top: '0', left: '0', opacity: hovering ? '1' : '0', borderRadius: '4px' }} />
-        <canvas ref={this.setCanvasRef} style={{ borderRadius: '4px', width: this.props.size, height: this.props.size, opacity: hovering ? '0' : '1' }} />
-      </div>
+      <div
+        className='avatar'
+        onMouseEnter={this.handleMouseEnter}
+        onMouseLeave={this.handleMouseLeave}
+        style={style}
+      />
     );
   }
 
diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx
index 60bf531e5..c4d5f829b 100644
--- a/app/assets/javascripts/components/components/status.jsx
+++ b/app/assets/javascripts/components/components/status.jsx
@@ -91,7 +91,7 @@ const Status = React.createClass({
 
           <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}>
             <div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
-              <Avatar src={status.getIn(['account', 'avatar'])} size={48} />
+              <Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
             </div>
 
             <DisplayName account={status.get('account')} />
diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx
index 6c25afdea..9cf03bb32 100644
--- a/app/assets/javascripts/components/components/status_content.jsx
+++ b/app/assets/javascripts/components/components/status_content.jsx
@@ -36,6 +36,7 @@ const StatusContent = React.createClass({
 
       if (mention) {
         link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
+        link.setAttribute('title', mention.get('acct'));
       } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
         link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
       } else if (media) {
@@ -91,7 +92,7 @@ const StatusContent = React.createClass({
     const { status } = this.props;
     const { hidden } = this.state;
 
-    const content = { __html: emojify(status.get('content')) };
+    const content = { __html: emojify(status.get('content')).replace(/\n/g, '') };
     const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
     const directionStyle = { direction: 'ltr' };
 
@@ -125,7 +126,7 @@ const StatusContent = React.createClass({
           <div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} />
         </div>
       );
-    } else {
+    } else if (this.props.onClick) {
       return (
         <div
           className='status__content'
@@ -135,6 +136,14 @@ const StatusContent = React.createClass({
           dangerouslySetInnerHTML={content}
         />
       );
+    } else {
+      return (
+        <div
+          className='status__content'
+          style={{ ...directionStyle }}
+          dangerouslySetInnerHTML={content}
+        />
+      );
     }
   },
 
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index 00f20074d..fea8b1594 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -48,6 +48,8 @@ import hu from 'react-intl/locale-data/hu';
 import uk from 'react-intl/locale-data/uk';
 import fi from 'react-intl/locale-data/fi';
 import eo from 'react-intl/locale-data/eo';
+import ru from 'react-intl/locale-data/ru';
+
 import getMessagesForLocale from '../locales';
 import { hydrateStore } from '../actions/store';
 import createStream from '../stream';
@@ -60,7 +62,9 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
   basename: '/web'
 });
 
-addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo]);
+
+addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo, ...ru]);
+
 
 const Mastodon = React.createClass({
 
diff --git a/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx
index 5591b45cf..9e05193fb 100644
--- a/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx
+++ b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx
@@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 
 const AutosuggestAccount = ({ account }) => (
   <div style={{ overflow: 'hidden' }} className='autosuggest-account'>
-    <div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div>
+    <div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={18} /></div>
     <DisplayName account={account} />
   </div>
 );
diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
index b016d3f28..cb4b62f6c 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -83,11 +83,23 @@ const ComposeForm = React.createClass({
     this.props.onChangeSpoilerText(e.target.value);
   },
 
+  componentWillReceiveProps (nextProps) {
+    // If this is the update where we've finished uploading,
+    // save the last caret position so we can restore it below!
+    if (!nextProps.is_uploading && this.props.is_uploading) {
+      this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart;
+    }
+  },
+
   componentDidUpdate (prevProps) {
-    if (this.props.focusDate !== prevProps.focusDate) {
-      // If replying to zero or one users, places the cursor at the end of the textbox.
-      // If replying to more than one user, selects any usernames past the first;
-      // this provides a convenient shortcut to drop everyone else from the conversation.
+    // This statement does several things: 
+    // - If we're beginning a reply, and,
+    //     - Replying to zero or one users, places the cursor at the end of the textbox.
+    //     - Replying to more than one user, selects any usernames past the first;
+    //       this provides a convenient shortcut to drop everyone else from the conversation.
+    // - If we've just finished uploading an image, and have a saved caret position,
+    //   restores the cursor to that position after the text changes!
+    if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) {
       let selectionEnd, selectionStart;
 
       if (this.props.preselectDate !== prevProps.preselectDate) {
@@ -118,7 +130,7 @@ const ComposeForm = React.createClass({
 
   render () {
     const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
-    const disabled = this.props.is_submitting || this.props.is_uploading;
+    const disabled = this.props.is_submitting;
 
     let publishText    = '';
     let privacyWarning = '';
diff --git a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx
index 1920b29bf..fa577ce26 100644
--- a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx
+++ b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx
@@ -47,7 +47,7 @@ const EmojiPickerDropdown = React.createClass({
         </DropdownTrigger>
 
         <DropdownContent className='dropdown__left'>
-          <EmojiPicker emojione={settings} onChange={this.handleChange} />
+          <EmojiPicker emojione={settings} onChange={this.handleChange} search={true} />
         </DropdownContent>
       </Dropdown>
     );
diff --git a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx
index 076ac7cbb..1a748a23c 100644
--- a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx
+++ b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx
@@ -17,7 +17,7 @@ const NavigationBar = React.createClass({
   render () {
     return (
       <div className='navigation-bar'>
-        <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink>
+        <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink>
 
         <div style={{ flex: '1 1 auto', marginLeft: '8px' }}>
           <strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong>
diff --git a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx
index a72bd32c2..11a89449e 100644
--- a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx
+++ b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx
@@ -50,7 +50,7 @@ const ReplyIndicator = React.createClass({
           <div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
 
           <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
-            <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} /></div>
+            <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
             <DisplayName account={status.get('account')} />
           </a>
         </div>
diff --git a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
index 1766655c2..9c713287c 100644
--- a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
+++ b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
@@ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
     <div>
       <div style={outerStyle}>
         <Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
-          <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={48} /></div>
+          <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div>
           <DisplayName account={account} />
         </Permalink>
 
diff --git a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
index 62c3e61e0..debbfd01f 100644
--- a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
@@ -4,16 +4,6 @@ const messages = defineMessages({
   clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }
 });
 
-const iconStyle = {
-  fontSize: '16px',
-  padding: '15px',
-  position: 'absolute',
-  right: '48px',
-  top: '0',
-  cursor: 'pointer',
-  zIndex: '2'
-};
-
 const ClearColumnButton = React.createClass({
 
   propTypes: {
@@ -25,7 +15,7 @@ const ClearColumnButton = React.createClass({
     const { intl } = this.props;
 
     return (
-      <div title={intl.formatMessage(messages.clear)} className='column-icon' tabIndex='0' style={iconStyle} onClick={this.onClick}>
+      <div title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
         <i className='fa fa-eraser' />
       </div>
     );
diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx
index 0de4df52e..0607466d0 100644
--- a/app/assets/javascripts/components/features/notifications/components/notification.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/notification.jsx
@@ -21,7 +21,7 @@ const Notification = React.createClass({
 
   renderFollow (account, link) {
     return (
-      <div className='notification'>
+      <div className='notification notification-follow'>
         <div className='notification__message'>
           <div style={{ position: 'absolute', 'left': '-26px'}}>
             <i className='fa fa-fw fa-user-plus' />
@@ -41,7 +41,7 @@ const Notification = React.createClass({
 
   renderFavourite (notification, link) {
     return (
-      <div className='notification'>
+      <div className='notification notification-favourite'>
         <div className='notification__message'>
           <div style={{ position: 'absolute', 'left': '-26px'}}>
             <i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} />
@@ -57,7 +57,7 @@ const Notification = React.createClass({
 
   renderReblog (notification, link) {
     return (
-      <div className='notification'>
+      <div className='notification notification-reblog'>
         <div className='notification__message'>
           <div style={{ position: 'absolute', 'left': '-26px'}}>
             <i className='fa fa-fw fa-retweet' />
@@ -76,7 +76,7 @@ const Notification = React.createClass({
     const account          = notification.get('account');
     const displayName      = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
     const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
-    const link             = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
+    const link             = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
 
     switch(notification.get('type')) {
       case 'follow':
diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
index caa46ff3c..2da57252e 100644
--- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx
+++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
@@ -54,7 +54,7 @@ const DetailedStatus = React.createClass({
     return (
       <div style={{ padding: '14px 10px' }} className='detailed-status'>
         <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
-          <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} size={48} /></div>
+          <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div>
           <DisplayName account={status.get('account')} />
         </a>
 
diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx
index fdd9c0e00..568422ff3 100644
--- a/app/assets/javascripts/components/locales/fr.jsx
+++ b/app/assets/javascripts/components/locales/fr.jsx
@@ -14,6 +14,7 @@ const fr = {
   "status.show_less": "Replier",
   "status.open": "Déplier ce status",
   "status.report": "Signaler @{name}",
+  "status.load_more": "Charger plus",
   "video_player.toggle_sound": "Mettre/Couper le son",
   "account.mention": "Mentionner",
   "account.edit_profile": "Modifier le profil",
@@ -41,6 +42,7 @@ const fr = {
   "column.notifications": "Notifications",
   "column.blocks": "Utilisateurs bloqués",
   "column.favourites": "Favoris",
+  "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.",
   "tabs_bar.compose": "Composer",
   "tabs_bar.home": "Accueil",
   "tabs_bar.mentions": "Mentions",
diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx
index 1e7b8b548..f9e1fe5bd 100644
--- a/app/assets/javascripts/components/locales/index.jsx
+++ b/app/assets/javascripts/components/locales/index.jsx
@@ -7,6 +7,8 @@ import pt from './pt';
 import uk from './uk';
 import fi from './fi';
 import eo from './eo';
+import ru from './ru';
+
 
 const locales = {
   en,
@@ -17,7 +19,9 @@ const locales = {
   pt,
   uk,
   fi,
-  eo
+  eo,
+  ru
+
 };
 
 export default function getMessagesForLocale (locale) {
diff --git a/app/assets/javascripts/components/locales/pt.jsx b/app/assets/javascripts/components/locales/pt.jsx
index d68724b13..8d1b88c75 100644
--- a/app/assets/javascripts/components/locales/pt.jsx
+++ b/app/assets/javascripts/components/locales/pt.jsx
@@ -2,54 +2,71 @@ const pt = {
   "column_back_button.label": "Voltar",
   "lightbox.close": "Fechar",
   "loading_indicator.label": "Carregando...",
-  "status.mention": "Menção",
-  "status.delete": "Deletar",
+  "status.mention": "Mencionar @{name}",
+  "status.delete": "Eliminar",
   "status.reply": "Responder",
-  "status.reblog": "Reblogar",
-  "status.favourite": "Favoritar",
-  "status.reblogged_by": "{name} reblogou",
-  "video_player.toggle_sound": "Alterar som",
-  "account.mention": "Menção",
+  "status.reblog": "Partilhar",
+  "status.favourite": "Adicionar aos favoritos",
+  "status.reblogged_by": "{name} partilhou",
+  "status.sensitive_warning": "Conteúdo sensível",
+  "status.sensitive_toggle": "Clique para ver",
+  "status.show_more": "Mostrar mais",
+  "status.show_less": "Mostrar menos",
+  "status.open": "Expandir",
+  "status.report": "Reportar @{name}",
+  "video_player.toggle_sound": "Ligar/Desligar som",
+  "account.mention": "Mencionar @{name}",
   "account.edit_profile": "Editar perfil",
-  "account.unblock": "Desbloquear",
-  "account.unfollow": "Unfollow",
-  "account.block": "Bloquear",
+  "account.unblock": "Não bloquear @{name}",
+  "account.unfollow": "Não seguir",
+  "account.block": "Bloquear @{name}",
   "account.follow": "Seguir",
-  "account.block": "Bloquear",
   "account.posts": "Posts",
   "account.follows": "Segue",
   "account.followers": "Seguidores",
-  "account.follows_you": "Segue você",
+  "account.follows_you": "É teu seguidor",
+  "account.requested": "A aguardar aprovação",
   "getting_started.heading": "Primeiros passos",
-  "getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão entrando um endereço similar a e-mail no campo no topo da barra lateral.",
+  "getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão colocando um endereço similar a e-mail no campo no topo da barra lateral.",
   "getting_started.about_shortcuts": "Se o usuário alvo está no mesmo domínio, só o nome funcionará. A mesma regra se aplica a mencionar pessoas nas postagens.",
-  "getting_started.about_developer": "O desenvolvedor desse projeto pode ser seguido em Gargron@mastodon.social",
+  "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.",
   "column.home": "Home",
-  "column.mentions": "Menções",
+  "column.community": "Local",
   "column.public": "Público",
-  "tabs_bar.compose": "Compôr",
+  "column.notifications": "Notificações",
+  "tabs_bar.compose": "Criar",
   "tabs_bar.home": "Home",
   "tabs_bar.mentions": "Menções",
   "tabs_bar.public": "Público",
   "tabs_bar.notifications": "Notificações",
-  "compose_form.placeholder": "Que estás pensando?",
+  "compose_form.placeholder": "Em que estás a pensar?",
   "compose_form.publish": "Publicar",
-  "compose_form.sensitive": "Marcar conteúdo como sensível",
-  "compose_form.unlisted": "Modo não-listado",
+  "compose_form.sensitive": "Media com conteúdo sensível",
+  "compose_form.spoiler": "Esconder texto com aviso",
+  "compose_form.private": "Tornar privado",
+  "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.",
+  "compose_form.unlisted": "Não mostrar na listagem pública",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.preferences": "Preferências",
-  "navigation_bar.public_timeline": "Timeline Pública",
-  "navigation_bar.logout": "Logout",
+  "navigation_bar.community_timeline": "Local",
+  "navigation_bar.public_timeline": "Público",
+  "navigation_bar.logout": "Sair",
   "reply_indicator.cancel": "Cancelar",
-  "search.placeholder": "Busca",
+  "search.placeholder": "Pesquisar",
   "search.account": "Conta",
   "search.hashtag": "Hashtag",
   "upload_button.label": "Adicionar media",
-  "upload_form.undo": "Desfazer",
-  "notification.follow": "{name} seguiu você",
-  "notification.favourite": "{name} favoritou  seu post",
-  "notification.reblog": "{name} reblogou o seu post",
-  "notification.mention": "{name} mecionou você"
+  "upload_form.undo": "Anular",
+  "notification.follow": "{name} seguiu-te",
+  "notification.favourite": "{name} adicionou o teu post aos favoritos",
+  "notification.reblog": "{name} partilhou o teu post",
+  "notification.mention": "{name} mencionou-te",
+  "notifications.column_settings.alert": "Notificações no computador",
+  "notifications.column_settings.show": "Mostrar nas colunas",
+  "notifications.column_settings.follow": "Novos seguidores:",
+  "notifications.column_settings.favourite": "Favoritos:",
+  "notifications.column_settings.mention": "Menções:",
+  "notifications.column_settings.reblog": "Partilhas:",
 };
 
 export default pt;
diff --git a/app/assets/javascripts/components/locales/ru.jsx b/app/assets/javascripts/components/locales/ru.jsx
new file mode 100644
index 000000000..e109005a7
--- /dev/null
+++ b/app/assets/javascripts/components/locales/ru.jsx
@@ -0,0 +1,68 @@
+const ru = {
+  "column_back_button.label": "Назад",
+  "lightbox.close": "Закрыть",
+  "loading_indicator.label": "Загрузка...",
+  "status.mention": "Упомянуть @{name}",
+  "status.delete": "Удалить",
+  "status.reply": "Ответить",
+  "status.reblog": "Продвинуть",
+  "status.favourite": "Нравится",
+  "status.reblogged_by": "{name} продвинул(а)",
+  "status.sensitive_warning": "Чувствительный контент",
+  "status.sensitive_toggle": "Нажмите для просмотра",
+  "video_player.toggle_sound": "Вкл./выкл. звук",
+  "account.mention": "Упомянуть @{name}",
+  "account.edit_profile": "Изменить профиль",
+  "account.unblock": "Разблокировать @{name}",
+  "account.unfollow": "Отписаться",
+  "account.block": "Блокировать @{name}",
+  "account.follow": "Подписаться",
+  "account.posts": "Посты",
+  "account.follows": "Подписки",
+  "account.followers": "Подписчики",
+  "account.follows_you": "Подписан(а) на Вас",
+  "account.requested": "Ожидает подтверждения",
+  "getting_started.heading": "Добро пожаловать",
+  "getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.",
+  "getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.",
+  "getting_started.open_source_notice": "Mastodon - программа с открытым исходным кодом. Вы можете помочь проекту или сообщить о проблемах на GitHub по адресу {github}. {apps}.",
+  "column.home": "Главная",
+  "column.community": "Локальная лента",
+  "column.public": "Глобальная лента",
+  "column.notifications": "Уведомления",
+  "tabs_bar.compose": "Написать",
+  "tabs_bar.home": "Главная",
+  "tabs_bar.mentions": "Упоминания",
+  "tabs_bar.public": "Глобальная лента",
+  "tabs_bar.notifications": "Уведомления",
+  "compose_form.placeholder": "О чем Вы думаете?",
+  "compose_form.publish": "Протрубить",
+  "compose_form.sensitive": "Отметить как чувствительный контент",
+  "compose_form.spoiler": "Скрыть текст за предупреждением",
+  "compose_form.private": "Отметить как приватное",
+  "compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.",
+  "compose_form.unlisted": "Не отображать в публичных лентах",
+  "navigation_bar.edit_profile": "Изменить профиль",
+  "navigation_bar.preferences": "Опции",
+  "navigation_bar.community_timeline": "Локальная лента",
+  "navigation_bar.public_timeline": "Глобальная лента",
+  "navigation_bar.logout": "Выйти",
+  "reply_indicator.cancel": "Отмена",
+  "search.placeholder": "Поиск",
+  "search.account": "Аккаунт",
+  "search.hashtag": "Хэштег",
+  "upload_button.label": "Добавить медиаконтент",
+  "upload_form.undo": "Отменить",
+  "notification.follow": "{name} подписался(-лась) на Вас",
+  "notification.favourite": "{name} понравился Ваш статус",
+  "notification.reblog": "{name} продвинул(а) Ваш статус",
+  "notification.mention": "{name} упомянул(а) Вас",
+  "notifications.column_settings.alert": "Десктопные уведомления",
+  "notifications.column_settings.show": "Показывать в колонке",
+  "notifications.column_settings.follow": "Новые подписчики:",
+  "notifications.column_settings.favourite": "Нравится:",
+  "notifications.column_settings.mention": "Упоминания:",
+  "notifications.column_settings.reblog": "Продвижения:",
+};
+
+export default ru;
diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss
index b3ae33500..50181d86e 100644
--- a/app/assets/stylesheets/accounts.scss
+++ b/app/assets/stylesheets/accounts.scss
@@ -72,6 +72,7 @@
     position: relative;
     z-index: 2;
     flex-direction: row;
+    background: rgba(0,0,0,0.5);
   }
 
   .details-counters {
@@ -83,7 +84,7 @@
   .counter {
     width: 80px;
     color: $color3;
-    padding: 0 10px;
+    padding: 5px 10px 0px;
     margin-bottom: 10px;
     border-right: 1px solid $color3;
     cursor: default;
@@ -173,7 +174,7 @@
   text-align: center;
   overflow: hidden;
 
-  a, .current, .next_page, .previous_page, .gap {
+  a, .current, .page, .gap {
     font-size: 14px;
     color: $color5;
     font-weight: 500;
@@ -193,12 +194,12 @@
     cursor: default;
   }
 
-  .previous_page, .next_page {
+  .prev, .next {
     text-transform: uppercase;
     color: $color2;
   }
 
-  .previous_page {
+  .prev {
     float: left;
     padding-left: 0;
 
@@ -208,7 +209,7 @@
     }
   }
 
-  .next_page {
+  .next {
     float: right;
     padding-right: 0;
 
@@ -226,11 +227,11 @@
   @media screen and (max-width: 360px) {
     padding: 30px 20px;
 
-    a, .current, .next_page, .previous_page, .gap {
+    a, .current, .next, .prev, .gap {
       display: none;
     }
 
-    .next_page, .previous_page {
+    .next, .prev {
       display: inline-block;
     }
   }
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index d31f148a2..316398874 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -1,7 +1,7 @@
 @import 'variables';
 
 .app-body{
- -ms-overflow-style: -ms-autohiding-scrollbar; 
+ -ms-overflow-style: -ms-autohiding-scrollbar;
 }
 
 .button {
@@ -49,6 +49,22 @@
   }
 }
 
+.column-icon-clear {
+  font-size: 16px;
+  padding: 15px;
+  position: absolute;
+  right: 48px;
+  top: 0;
+  cursor: pointer;
+  z-index: 2;
+}
+
+@media screen and (min-width: 1024px) {
+  .column-icon-clear {
+    top: 10px;
+  }
+}
+
 .icon-button {
   display: inline-block;
   padding: 0;
@@ -149,6 +165,14 @@
   }
 }
 
+.avatar {
+  border-radius: 4px;
+  background: transparent no-repeat;
+  background-position: 50%;
+  background-clip: padding-box;
+  position: relative;
+}
+
 .lightbox .icon-button {
   color: $color1;
 }
@@ -714,7 +738,7 @@ a.status__content__spoiler-link {
 
 @media screen and (min-width: 360px) {
   .columns-area {
-    margin: 10px;
+    padding: 10px;
   }
 }
 
@@ -722,9 +746,12 @@ a.status__content__spoiler-link {
   width: 330px;
   position: relative;
   box-sizing: border-box;
-  background: $color1;
   display: flex;
   flex-direction: column;
+
+  > .scrollable {
+    background: $color1;
+  }
 }
 
 .ui {
@@ -756,6 +783,58 @@ a.status__content__spoiler-link {
   border-bottom: 2px solid transparent;
 }
 
+.column, .drawer {
+  flex: 1 1 100%;
+  overflow: hidden;
+}
+
+@media screen and (min-width: 360px) {
+  .tabs-bar {
+    margin: 10px;
+    margin-bottom: 0;
+  }
+
+  .search {
+    margin-bottom: 10px;
+  }
+}
+
+@media screen and (max-width: 1024px) {
+  .column, .drawer {
+    width: 100%;
+    padding: 0;
+  }
+
+  .columns-area {
+    flex-direction: column;
+  }
+
+  .search__input, .autosuggest-textarea__textarea {
+    font-size: 16px;
+  }
+}
+
+@media screen and (min-width: 1024px) {
+  .columns-area {
+    padding: 0;
+  }
+
+  .column, .drawer {
+    flex: 0 0 auto;
+    padding: 10px;
+    padding-left: 5px;
+    padding-right: 5px;
+
+    &:first-child {
+      padding-left: 10px;
+    }
+
+    &:last-child {
+      padding-right: 10px;
+    }
+  }
+}
+
 @media screen and (min-width: 2560px) {
   .columns-area {
     justify-content: center;
@@ -815,37 +894,6 @@ a.status__content__spoiler-link {
   }
 }
 
-.column, .drawer {
-  margin-left: 5px;
-  margin-right: 5px;
-  flex: 0 0 auto;
-  overflow: hidden;
-}
-
-.column:first-child, .drawer:first-child {
-  margin-left: 0;
-}
-
-.column:last-child, .drawer:last-child {
-  margin-right: 0;
-}
-
-@media screen and (max-width: 1024px) {
-  .column, .drawer {
-    width: 100%;
-    margin: 0;
-    flex: 1 1 100%;
-  }
-
-  .columns-area {
-    flex-direction: column;
-  }
-
-  .search__input, .autosuggest-textarea__textarea {
-    font-size: 16px;
-  }
-}
-
 .tabs-bar {
   display: flex;
   background: lighten($color1, 8%);
@@ -856,17 +904,18 @@ a.status__content__spoiler-link {
 .tabs-bar__link {
   display: block;
   flex: 1 1 auto;
-  padding: 10px 5px;
+  padding: 15px 10px;
   color: $color5;
   text-decoration: none;
   text-align: center;
-  font-size:12px;
+  font-size: 14px;
   font-weight: 500;
   border-bottom: 2px solid lighten($color1, 8%);
   transition: all 200ms linear;
 
   .fa {
     font-weight: 400;
+    font-size: 16px;
   }
 
   &.active {
@@ -880,27 +929,13 @@ a.status__content__spoiler-link {
   }
 
   span {
+    margin-left: 5px;
     display: none;
   }
 }
 
-@media screen and (min-width: 360px) {
-  .tabs-bar {
-    margin: 10px;
-    margin-bottom: 0;
-  }
-
-  .search {
-    margin-bottom: 10px;
-  }
-}
-
 @media screen and (min-width: 600px) {
   .tabs-bar__link {
-    .fa {
-      margin-right: 5px;
-    }
-
     span {
       display: inline;
     }
@@ -1362,12 +1397,15 @@ button.icon-button.active i.fa-retweet {
 
 .empty-column-indicator {
   color: lighten($color1, 20%);
+  background: $color1;
   text-align: center;
   padding: 20px;
-  padding-top: 100px;
   font-size: 15px;
   font-weight: 400;
   cursor: default;
+  display: flex;
+  flex: 1 1 auto;
+  align-items: center;
 
   a {
     color: $color4;
@@ -1395,7 +1433,7 @@ button.icon-button.active i.fa-retweet {
 .emoji-dialog {
   width: 280px;
   height: 220px;
-  background: $color2;
+  background: darken($color3, 10%);
   box-sizing: border-box;
   border-radius: 2px;
   overflow: hidden;
@@ -1404,6 +1442,8 @@ button.icon-button.active i.fa-retweet {
 
   .emojione {
     margin: 0;
+    width: 100%;
+    height: auto;
   }
 
   .emoji-dialog-header {
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index 7fd43489f..04e7ddacf 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -2,30 +2,25 @@
 
 class AboutController < ApplicationController
   before_action :set_body_classes
+  before_action :set_instance_presenter, only: [:show, :more]
 
-  def index
-    @description                  = Setting.site_description
-    @open_registrations           = Setting.open_registrations
-    @closed_registrations_message = Setting.closed_registrations_message
+  def show; end
 
-    @user = User.new
-    @user.build_account
-  end
-
-  def more
-    @description          = Setting.site_description
-    @extended_description = Setting.site_extended_description
-    @contact_account      = Account.find_local(Setting.site_contact_username)
-    @contact_email        = Setting.site_contact_email
-    @user_count           = Rails.cache.fetch('user_count')            { User.count }
-    @status_count         = Rails.cache.fetch('local_status_count')    { Status.local.count }
-    @domain_count         = Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) }
-  end
+  def more; end
 
   def terms; end
 
   private
 
+  def new_user
+    User.new.tap(&:build_account)
+  end
+  helper_method :new_user
+
+  def set_instance_presenter
+    @instance_presenter = InstancePresenter.new
+  end
+
   def set_body_classes
     @body_classes = 'about-body'
   end
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 619c04be2..d4f157614 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -35,11 +35,11 @@ class AccountsController < ApplicationController
   end
 
   def followers
-    @followers = @account.followers.order('follows.created_at desc').paginate(page: params[:page], per_page: 12)
+    @followers = @account.followers.order('follows.created_at desc').page(params[:page]).per(12)
   end
 
   def following
-    @following = @account.following.order('follows.created_at desc').paginate(page: params[:page], per_page: 12)
+    @following = @account.following.order('follows.created_at desc').page(params[:page]).per(12)
   end
 
   private
@@ -53,7 +53,7 @@ class AccountsController < ApplicationController
   end
 
   def webfinger_account_url
-    webfinger_url(resource: "acct:#{@account.acct}@#{Rails.configuration.x.local_domain}")
+    webfinger_url(resource: @account.to_webfinger_s)
   end
 
   def check_account_suspension
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index df2c7bebf..71cb8edd8 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -1,51 +1,50 @@
 # frozen_string_literal: true
 
-class Admin::AccountsController < ApplicationController
-  before_action :require_admin!
-  before_action :set_account, except: :index
-
-  layout 'admin'
-
-  def index
-    @accounts = Account.alphabetic.paginate(page: params[:page], per_page: 40)
-
-    @accounts = @accounts.local                             if params[:local].present?
-    @accounts = @accounts.remote                            if params[:remote].present?
-    @accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present?
-    @accounts = @accounts.silenced                          if params[:silenced].present?
-    @accounts = @accounts.recent                            if params[:recent].present?
-    @accounts = @accounts.suspended                         if params[:suspended].present?
-  end
-
-  def show; end
-
-  def suspend
-    Admin::SuspensionWorker.perform_async(@account.id)
-    redirect_to admin_accounts_path
-  end
-
-  def unsuspend
-    @account.update(suspended: false)
-    redirect_to admin_accounts_path
-  end
-
-  def silence
-    @account.update(silenced: true)
-    redirect_to admin_accounts_path
-  end
-
-  def unsilence
-    @account.update(silenced: false)
-    redirect_to admin_accounts_path
-  end
-
-  private
-
-  def set_account
-    @account = Account.find(params[:id])
-  end
-
-  def account_params
-    params.require(:account).permit(:silenced, :suspended)
+module Admin
+  class AccountsController < BaseController
+    before_action :set_account, except: :index
+
+    def index
+      @accounts = Account.alphabetic.page(params[:page])
+
+      @accounts = @accounts.local                             if params[:local].present?
+      @accounts = @accounts.remote                            if params[:remote].present?
+      @accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present?
+      @accounts = @accounts.silenced                          if params[:silenced].present?
+      @accounts = @accounts.recent                            if params[:recent].present?
+      @accounts = @accounts.suspended                         if params[:suspended].present?
+    end
+
+    def show; end
+
+    def suspend
+      Admin::SuspensionWorker.perform_async(@account.id)
+      redirect_to admin_accounts_path
+    end
+
+    def unsuspend
+      @account.update(suspended: false)
+      redirect_to admin_accounts_path
+    end
+
+    def silence
+      @account.update(silenced: true)
+      redirect_to admin_accounts_path
+    end
+
+    def unsilence
+      @account.update(silenced: false)
+      redirect_to admin_accounts_path
+    end
+
+    private
+
+    def set_account
+      @account = Account.find(params[:id])
+    end
+
+    def account_params
+      params.require(:account).permit(:silenced, :suspended)
+    end
   end
 end
diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb
new file mode 100644
index 000000000..11fe326bc
--- /dev/null
+++ b/app/controllers/admin/base_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Admin
+  class BaseController < ApplicationController
+    before_action :require_admin!
+
+    layout 'admin'
+  end
+end
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index 1f4432847..a8b56c085 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -1,32 +1,30 @@
 # frozen_string_literal: true
 
-class Admin::DomainBlocksController < ApplicationController
-  before_action :require_admin!
-
-  layout 'admin'
-
-  def index
-    @blocks = DomainBlock.paginate(page: params[:page], per_page: 40)
-  end
+module Admin
+  class DomainBlocksController < BaseController
+    def index
+      @blocks = DomainBlock.page(params[:page])
+    end
 
-  def new
-    @domain_block = DomainBlock.new
-  end
+    def new
+      @domain_block = DomainBlock.new
+    end
 
-  def create
-    @domain_block = DomainBlock.new(resource_params)
+    def create
+      @domain_block = DomainBlock.new(resource_params)
 
-    if @domain_block.save
-      DomainBlockWorker.perform_async(@domain_block.id)
-      redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
-    else
-      render action: :new
+      if @domain_block.save
+        DomainBlockWorker.perform_async(@domain_block.id)
+        redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
+      else
+        render action: :new
+      end
     end
-  end
 
-  private
+    private
 
-  def resource_params
-    params.require(:domain_block).permit(:domain, :severity)
+    def resource_params
+      params.require(:domain_block).permit(:domain, :severity)
+    end
   end
 end
diff --git a/app/controllers/admin/pubsubhubbub_controller.rb b/app/controllers/admin/pubsubhubbub_controller.rb
index b9e840ffe..31c80a174 100644
--- a/app/controllers/admin/pubsubhubbub_controller.rb
+++ b/app/controllers/admin/pubsubhubbub_controller.rb
@@ -1,11 +1,9 @@
 # frozen_string_literal: true
 
-class Admin::PubsubhubbubController < ApplicationController
-  before_action :require_admin!
-
-  layout 'admin'
-
-  def index
-    @subscriptions = Subscription.order('id desc').includes(:account).paginate(page: params[:page], per_page: 40)
+module Admin
+  class PubsubhubbubController < BaseController
+    def index
+      @subscriptions = Subscription.order('id desc').includes(:account).page(params[:page])
+    end
   end
 end
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index 2b3b1809f..3c3082318 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -1,45 +1,44 @@
 # frozen_string_literal: true
 
-class Admin::ReportsController < ApplicationController
-  before_action :require_admin!
-  before_action :set_report, except: [:index]
-
-  layout 'admin'
-
-  def index
-    @reports = Report.includes(:account, :target_account).order('id desc').paginate(page: params[:page], per_page: 40)
-    @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved
-  end
-
-  def show
-    @statuses = Status.where(id: @report.status_ids)
-  end
-
-  def resolve
-    @report.update(action_taken: true, action_taken_by_account_id: current_account.id)
-    redirect_to admin_report_path(@report)
-  end
-
-  def suspend
-    Admin::SuspensionWorker.perform_async(@report.target_account.id)
-    Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
-    redirect_to admin_report_path(@report)
-  end
-
-  def silence
-    @report.target_account.update(silenced: true)
-    Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
-    redirect_to admin_report_path(@report)
-  end
-
-  def remove
-    RemovalWorker.perform_async(params[:status_id])
-    redirect_to admin_report_path(@report)
-  end
-
-  private
-
-  def set_report
-    @report = Report.find(params[:id])
+module Admin
+  class ReportsController < BaseController
+    before_action :set_report, except: [:index]
+
+    def index
+      @reports = Report.includes(:account, :target_account).order('id desc').page(params[:page])
+      @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved
+    end
+
+    def show
+      @statuses = Status.where(id: @report.status_ids)
+    end
+
+    def resolve
+      @report.update(action_taken: true, action_taken_by_account_id: current_account.id)
+      redirect_to admin_report_path(@report)
+    end
+
+    def suspend
+      Admin::SuspensionWorker.perform_async(@report.target_account.id)
+      Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
+      redirect_to admin_report_path(@report)
+    end
+
+    def silence
+      @report.target_account.update(silenced: true)
+      Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
+      redirect_to admin_report_path(@report)
+    end
+
+    def remove
+      RemovalWorker.perform_async(params[:status_id])
+      redirect_to admin_report_path(@report)
+    end
+
+    private
+
+    def set_report
+      @report = Report.find(params[:id])
+    end
   end
 end
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index 7615c781d..6cca5c3e3 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -1,35 +1,33 @@
 # frozen_string_literal: true
 
-class Admin::SettingsController < ApplicationController
-  before_action :require_admin!
-
-  layout 'admin'
+module Admin
+  class SettingsController < BaseController
+    def index
+      @settings = Setting.all_as_records
+    end
 
-  def index
-    @settings = Setting.all_as_records
-  end
+    def update
+      @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id])
+      value    = settings_params[:value]
 
-  def update
-    @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id])
-    value    = settings_params[:value]
+      # Special cases
+      value = value == 'true' if @setting.var == 'open_registrations'
 
-    # Special cases
-    value = value == 'true' if @setting.var == 'open_registrations'
+      if @setting.value != value
+        @setting.value = value
+        @setting.save
+      end
 
-    if @setting.value != value
-      @setting.value = value
-      @setting.save
+      respond_to do |format|
+        format.html { redirect_to admin_settings_path }
+        format.json { respond_with_bip(@setting) }
+      end
     end
 
-    respond_to do |format|
-      format.html { redirect_to admin_settings_path }
-      format.json { respond_with_bip(@setting) }
-    end
-  end
-
-  private
+    private
 
-  def settings_params
-    params.require(:setting).permit(:value)
+    def settings_params
+      params.require(:setting).permit(:value)
+    end
   end
 end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 454873116..2c44e36a7 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -1,10 +1,11 @@
 # frozen_string_literal: true
 
 class Api::V1::AccountsController < ApiController
-  before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
+  before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute, :update_credentials]
   before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
+  before_action -> { doorkeeper_authorize! :write }, only: [:update_credentials]
   before_action :require_user!, except: [:show, :following, :followers, :statuses]
-  before_action :set_account, except: [:verify_credentials, :suggestions, :search]
+  before_action :set_account, except: [:verify_credentials, :update_credentials, :suggestions, :search]
 
   respond_to :json
 
@@ -15,6 +16,12 @@ class Api::V1::AccountsController < ApiController
     render action: :show
   end
 
+  def update_credentials
+    current_account.update!(account_params)
+    @account = current_account
+    render action: :show
+  end
+
   def following
     results   = Follow.where(account: @account).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
     accounts  = Account.where(id: results.map(&:target_account_id)).map { |a| [a.id, a] }.to_h
@@ -135,4 +142,8 @@ class Api::V1::AccountsController < ApiController
   def statuses_pagination_params(core_params)
     params.permit(:limit, :only_media, :exclude_replies).merge(core_params)
   end
+
+  def account_params
+    params.permit(:display_name, :note, :avatar, :header)
+  end
 end
diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb
index 71c054334..3cff29982 100644
--- a/app/controllers/api/v1/notifications_controller.rb
+++ b/app/controllers/api/v1/notifications_controller.rb
@@ -9,7 +9,7 @@ class Api::V1::NotificationsController < ApiController
   DEFAULT_NOTIFICATIONS_LIMIT = 15
 
   def index
-    @notifications = Notification.where(account: current_account).browserable.paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id])
+    @notifications = Notification.where(account: current_account).browserable(exclude_types).paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id])
     @notifications = cache_collection(@notifications, Notification)
     statuses       = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status)
 
@@ -32,7 +32,13 @@ class Api::V1::NotificationsController < ApiController
 
   private
 
+  def exclude_types
+    val = params.permit(exclude_types: [])[:exclude_types] || []
+    val = [val] unless val.is_a?(Enumerable)
+    val
+  end
+
   def pagination_params(core_params)
-    params.permit(:limit).merge(core_params)
+    params.permit(:limit, exclude_types: []).merge(core_params)
   end
 end
diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb
index db16f82e5..57604f1dc 100644
--- a/app/controllers/api_controller.rb
+++ b/app/controllers/api_controller.rb
@@ -7,6 +7,7 @@ class ApiController < ApplicationController
   protect_from_forgery with: :null_session
 
   skip_before_action :verify_authenticity_token
+  skip_before_action :store_current_location
 
   before_action :set_rate_limit_headers
 
diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb
index 6528ce45e..bcf3fd0a0 100644
--- a/app/controllers/concerns/localized.rb
+++ b/app/controllers/concerns/localized.rb
@@ -26,6 +26,8 @@ module Localized
   end
 
   def default_locale
-    ENV.fetch('DEFAULT_LOCALE') { I18n.default_locale }
+    ENV.fetch('DEFAULT_LOCALE') { 
+      http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale
+    }
   end
 end
diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb
index 1e3f786ec..22e376836 100644
--- a/app/controllers/remote_follow_controller.rb
+++ b/app/controllers/remote_follow_controller.rb
@@ -25,7 +25,7 @@ class RemoteFollowController < ApplicationController
 
       session[:remote_follow] = @remote_follow.acct
 
-      redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s
+      redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: @account.to_webfinger_s).to_s
     else
       render :new
     end
diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb
index 4fcec5322..ff688978c 100644
--- a/app/controllers/settings/exports_controller.rb
+++ b/app/controllers/settings/exports_controller.rb
@@ -39,7 +39,7 @@ class Settings::ExportsController < ApplicationController
   def accounts_list_to_csv(list)
     CSV.generate do |csv|
       list.each do |account|
-        csv << [(account.local? ? "#{account.username}@#{Rails.configuration.x.local_domain}" : account.acct)]
+        csv << [(account.local? ? account.local_username_and_domain : account.acct)]
       end
     end
   end
diff --git a/app/controllers/xrd_controller.rb b/app/controllers/xrd_controller.rb
index 6db87cefc..5964172e9 100644
--- a/app/controllers/xrd_controller.rb
+++ b/app/controllers/xrd_controller.rb
@@ -14,7 +14,7 @@ class XrdController < ApplicationController
 
   def webfinger
     @account = Account.find_local!(username_from_resource)
-    @canonical_account_uri = "acct:#{@account.username}@#{Rails.configuration.x.local_domain}"
+    @canonical_account_uri = @account.to_webfinger_s
     @magic_key = pem_to_magic_key(@account.keypair.public_key)
 
     respond_to do |format|
diff --git a/app/helpers/about_helper.rb b/app/helpers/about_helper.rb
deleted file mode 100644
index 0f57a7b5e..000000000
--- a/app/helpers/about_helper.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module AboutHelper
-end
diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb
deleted file mode 100644
index af23a78d1..000000000
--- a/app/helpers/accounts_helper.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module AccountsHelper
-  def pagination_options
-    {
-      previous_label: safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '),
-      next_label: safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '),
-      inner_window: 1,
-      outer_window: 0,
-    }
-  end
-end
diff --git a/app/helpers/admin/domain_blocks_helper.rb b/app/helpers/admin/domain_blocks_helper.rb
deleted file mode 100644
index d66c8d5e1..000000000
--- a/app/helpers/admin/domain_blocks_helper.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module Admin::DomainBlocksHelper
-end
diff --git a/app/helpers/admin/pubsubhubbub_helper.rb b/app/helpers/admin/pubsubhubbub_helper.rb
deleted file mode 100644
index c2fc2e7da..000000000
--- a/app/helpers/admin/pubsubhubbub_helper.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module Admin::PubsubhubbubHelper
-end
diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb
index b750eeb07..185388ec9 100644
--- a/app/helpers/atom_builder_helper.rb
+++ b/app/helpers/atom_builder_helper.rb
@@ -160,7 +160,7 @@ module AtomBuilderHelper
     object_type      xml, :person
     uri              xml, TagManager.instance.uri_for(account)
     name             xml, account.username
-    email            xml, account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct
+    email            xml, account.local? ? account.local_username_and_domain : account.acct
     summary          xml, account.note
     link_alternate   xml, TagManager.instance.url_for(account)
     link_avatar      xml, account
diff --git a/app/helpers/authorize_follow_helper.rb b/app/helpers/authorize_follow_helper.rb
deleted file mode 100644
index 99ee03c2f..000000000
--- a/app/helpers/authorize_follow_helper.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module AuthorizeFollowHelper
-end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 74dc0e11d..327ca4e98 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -5,13 +5,15 @@ module SettingsHelper
     en: 'English',
     de: 'Deutsch',
     es: 'Español',
+    eo: 'Esperanto',
     pt: 'Português',
     fr: 'Français',
     hu: 'Magyar',
     uk: 'Українська',
     'zh-CN': '简体中文',
     fi: 'Suomi',
-    eo: 'Esperanto',
+    ru: 'Русский',
+
   }.freeze
 
   def human_locale(locale)
diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb
deleted file mode 100644
index 5b2b3ca59..000000000
--- a/app/helpers/tags_helper.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module TagsHelper
-end
diff --git a/app/helpers/xrd_helper.rb b/app/helpers/xrd_helper.rb
deleted file mode 100644
index 2281a0278..000000000
--- a/app/helpers/xrd_helper.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module XrdHelper
-end
diff --git a/app/lib/atom_serializer.rb b/app/lib/atom_serializer.rb
index b9dcee6b3..68d2fce68 100644
--- a/app/lib/atom_serializer.rb
+++ b/app/lib/atom_serializer.rb
@@ -20,7 +20,7 @@ class AtomSerializer
     append_element(author, 'activity:object-type', TagManager::TYPES[:person])
     append_element(author, 'uri', uri)
     append_element(author, 'name', account.username)
-    append_element(author, 'email', account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct)
+    append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct)
     append_element(author, 'summary', account.note)
     append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account))
     append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original)))
@@ -67,7 +67,7 @@ class AtomSerializer
     append_element(entry, 'id', TagManager.instance.unique_tag(stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type))
     append_element(entry, 'published', stream_entry.created_at.iso8601)
     append_element(entry, 'updated', stream_entry.updated_at.iso8601)
-    append_element(entry, 'title', stream_entry&.status&.title)
+    append_element(entry, 'title', stream_entry&.status&.title || 'Delete')
 
     entry << author(stream_entry.account) if root
 
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 58d9fb1fc..339a5c78b 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -66,7 +66,7 @@ class FeedManager
     timeline_key = key(:home, into_account.id)
     oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
 
-    from_account.statuses.select('id').where('id > ?', oldest_home_score).find_in_batches do |statuses|
+    from_account.statuses.select('id').where('id > ?', oldest_home_score).reorder(nil).find_in_batches do |statuses|
       redis.pipelined do
         statuses.each do |status|
           redis.zrem(timeline_key, status.id)
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index da7ad2027..c3f331ff7 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -15,7 +15,6 @@ class Formatter
     html = status.text
     html = encode(html)
     html = simple_format(html, {}, sanitize: false)
-    html = html.gsub(/\n/, '')
     html = link_urls(html)
     html = link_mentions(html, status.mentions)
     html = link_hashtags(html)
diff --git a/app/models/account.rb b/app/models/account.rb
index cbba8b5b6..8ceda7f97 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -12,12 +12,12 @@ class Account < ApplicationRecord
   validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?'
 
   # Avatar upload
-  has_attached_file :avatar, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' }
+  has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-quality 80 -strip' }
   validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES
   validates_attachment_size :avatar, less_than: 2.megabytes
 
   # Header upload
-  has_attached_file :header, styles: { original: '700x335#' }, convert_options: { all: '-quality 80 -strip' }
+  has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-quality 80 -strip' }
   validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
   validates_attachment_size :header, less_than: 2.megabytes
 
@@ -120,6 +120,14 @@ class Account < ApplicationRecord
     local? ? username : "#{username}@#{domain}"
   end
 
+  def local_username_and_domain
+    "#{username}@#{Rails.configuration.x.local_domain}"
+  end
+
+  def to_webfinger_s
+    "acct:#{local_username_and_domain}"
+  end
+
   def subscribed?
     !subscription_expires_at.blank?
   end
@@ -150,6 +158,22 @@ class Account < ApplicationRecord
     save!
   end
 
+  def avatar_original_url
+    avatar.url(:original)
+  end
+
+  def avatar_static_url
+    avatar_content_type == 'image/gif' ? avatar.url(:static) : avatar_original_url
+  end
+
+  def header_original_url
+    header.url(:original)
+  end
+
+  def header_static_url
+    header_content_type == 'image/gif' ? header.url(:static) : header_original_url
+  end
+
   def avatar_remote_url=(url)
     parsed_url = URI.parse(url)
 
@@ -203,7 +227,7 @@ class Account < ApplicationRecord
     end
 
     def triadic_closures(account, limit = 5)
-      sql = <<SQL
+      sql = <<-SQL.squish
         WITH first_degree AS (
             SELECT target_account_id
             FROM follows
@@ -216,7 +240,7 @@ class Account < ApplicationRecord
         GROUP BY target_account_id, accounts.id
         ORDER BY count(account_id) DESC
         LIMIT ?
-SQL
+      SQL
 
       Account.find_by_sql([sql, account.id, account.id, limit])
     end
@@ -226,7 +250,7 @@ SQL
       textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))'
       query      = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')'
 
-      sql = <<SQL
+      sql = <<-SQL.squish
         SELECT
           accounts.*,
           ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
@@ -234,7 +258,7 @@ SQL
         WHERE #{query} @@ #{textsearch}
         ORDER BY rank DESC
         LIMIT ?
-SQL
+      SQL
 
       Account.find_by_sql([sql, limit])
     end
@@ -244,7 +268,7 @@ SQL
       textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))'
       query      = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')'
 
-      sql = <<SQL
+      sql = <<-SQL.squish
         SELECT
           accounts.*,
           (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
@@ -254,7 +278,7 @@ SQL
         GROUP BY accounts.id
         ORDER BY rank DESC
         LIMIT ?
-SQL
+      SQL
 
       Account.find_by_sql([sql, account.id, account.id, limit])
     end
@@ -284,6 +308,18 @@ SQL
     def follow_mapping(query, field)
       query.pluck(field).inject({}) { |mapping, id| mapping[id] = true; mapping }
     end
+
+    def avatar_styles(file)
+      styles = { original: '120x120#' }
+      styles[:static] = { format: 'png' } if file.content_type == 'image/gif'
+      styles
+    end
+
+    def header_styles(file)
+      styles = { original: '700x335#' }
+      styles[:static] = { format: 'png' } if file.content_type == 'image/gif'
+      styles
+    end
   end
 
   before_create do
diff --git a/app/models/notification.rb b/app/models/notification.rb
index b7b474869..302d4382d 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -16,10 +16,17 @@ class Notification < ApplicationRecord
 
   validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
 
+  TYPE_CLASS_MAP = {
+    mention:        'Mention',
+    reblog:         'Status',
+    follow:         'Follow',
+    follow_request: 'FollowRequest',
+    favourite:      'Favourite',
+  }.freeze
+
   STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze
 
   scope :cache_ids, -> { select(:id, :updated_at, :activity_type, :activity_id) }
-  scope :browserable, -> { where.not(activity_type: ['FollowRequest']) }
 
   cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account
 
@@ -28,12 +35,7 @@ class Notification < ApplicationRecord
   end
 
   def type
-    case activity_type
-    when 'Status'
-      :reblog
-    else
-      activity_type.underscore.to_sym
-    end
+    @type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
   end
 
   def target_status
@@ -50,6 +52,11 @@ class Notification < ApplicationRecord
   end
 
   class << self
+    def browserable(types = [])
+      types.concat([:follow_request])
+      where.not(activity_type: activity_types_from_types(types))
+    end
+
     def reload_stale_associations!(cached_items)
       account_ids = cached_items.map(&:from_account_id).uniq
       accounts    = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
@@ -58,6 +65,12 @@ class Notification < ApplicationRecord
         item.from_account = accounts[item.from_account_id]
       end
     end
+
+    private
+
+    def activity_types_from_types(types)
+      types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact
+    end
   end
 
   after_initialize :set_from_account
diff --git a/app/models/status.rb b/app/models/status.rb
index 7e3dd3e28..16cd4383f 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -75,7 +75,7 @@ class Status < ApplicationRecord
   end
 
   def title
-    content
+    reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
   end
 
   def hidden?
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 15625ca43..6209d7dab 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -17,7 +17,7 @@ class Tag < ApplicationRecord
       textsearch = 'to_tsvector(\'simple\', tags.name)'
       query      = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')'
 
-      sql = <<SQL
+      sql = <<-SQL.squish
         SELECT
           tags.*,
           ts_rank_cd(#{textsearch}, #{query}) AS rank
@@ -25,7 +25,7 @@ class Tag < ApplicationRecord
         WHERE #{query} @@ #{textsearch}
         ORDER BY rank DESC
         LIMIT ?
-SQL
+      SQL
 
       Tag.find_by_sql([sql, limit])
     end
diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb
new file mode 100644
index 000000000..cd809566f
--- /dev/null
+++ b/app/presenters/instance_presenter.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class InstancePresenter
+  delegate(
+    :closed_registrations_message,
+    :contact_email,
+    :open_registrations,
+    :site_description,
+    :site_extended_description,
+    to: Setting
+  )
+
+  def contact_account
+    Account.find_local(Setting.site_contact_username)
+  end
+
+  def user_count
+    Rails.cache.fetch('user_count') { User.count }
+  end
+
+  def status_count
+    Rails.cache.fetch('local_status_count') { Status.local.count }
+  end
+
+  def domain_count
+    Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) }
+  end
+end
diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb
index 6a6a696d6..50ffc47c6 100644
--- a/app/services/fetch_remote_account_service.rb
+++ b/app/services/fetch_remote_account_service.rb
@@ -19,11 +19,16 @@ class FetchRemoteAccountService < BaseService
     xml = Nokogiri::XML(body)
     xml.encoding = 'utf-8'
 
-    url_parts = Addressable::URI.parse(url)
-    username  = xml.at_xpath('//xmlns:author/xmlns:name').try(:content)
-    domain    = url_parts.host
+    email = xml.at_xpath('//xmlns:author/xmlns:email').try(:content)
+    if email.nil?
+      url_parts = Addressable::URI.parse(url)
+      username  = xml.at_xpath('//xmlns:author/xmlns:name').try(:content)
+      domain    = url_parts.host
+    else
+      username, domain = email.split('@')
+    end
 
-    return nil if username.nil?
+    return nil if username.nil? || domain.nil?
 
     Rails.logger.debug "Going to webfinger #{username}@#{domain}"
 
diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml
new file mode 100644
index 000000000..c7a9a488b
--- /dev/null
+++ b/app/views/about/_registration.html.haml
@@ -0,0 +1,30 @@
+= simple_form_for(new_user, url: user_registration_path) do |f|
+  = f.simple_fields_for :account do |account_fields|
+    = account_fields.input :username,
+      autofocus: true,
+      placeholder: t('simple_form.labels.defaults.username'),
+      required: true,
+      input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
+
+  = f.input :email,
+    placeholder: t('simple_form.labels.defaults.email'),
+    required: true,
+    input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
+  = f.input :password,
+    autocomplete: "off",
+    placeholder: t('simple_form.labels.defaults.password'),
+    required: true,
+    input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
+  = f.input :password_confirmation,
+    autocomplete: "off",
+    placeholder: t('simple_form.labels.defaults.confirm_password'),
+    required: true,
+    input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') }
+
+  .actions
+    = f.button :button, t('about.get_started'), type: :submit
+
+  .info
+    = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
+    ·
+    = link_to t('about.about_this'), about_more_path
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index 2de3bf986..8c12f57c1 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -7,42 +7,42 @@
       .panel
         %h2= Rails.configuration.x.local_domain
 
-        - unless @description.blank?
-          %p= @description.html_safe
+        - unless @instance_presenter.site_description.blank?
+          %p= @instance_presenter.site_description.html_safe
 
       .information-board
         .section
           %span= t 'about.user_count_before'
-          %strong= number_with_delimiter @user_count
+          %strong= number_with_delimiter @instance_presenter.user_count
           %span= t 'about.user_count_after'
         .section
           %span= t 'about.status_count_before'
-          %strong= number_with_delimiter @status_count
+          %strong= number_with_delimiter @instance_presenter.status_count
           %span= t 'about.status_count_after'
         .section
           %span= t 'about.domain_count_before'
-          %strong= number_with_delimiter @domain_count
+          %strong= number_with_delimiter @instance_presenter.domain_count
           %span= t 'about.domain_count_after'
 
-      - unless @extended_description.blank?
-        .panel= @extended_description.html_safe
+      - unless @instance_presenter.site_extended_description.blank?
+        .panel= @instance_presenter.site_extended_description.html_safe
 
     .sidebar
       .panel
         .panel-header= t 'about.contact'
         .panel-body
-          - if @contact_account
+          - if @instance_presenter.contact_account
             .owner
-              .avatar= image_tag @contact_account.avatar.url
+              .avatar= image_tag @instance_presenter.contact_account.avatar.url
               .name
-                = link_to TagManager.instance.url_for(@contact_account) do
-                  %span.display_name.emojify= display_name(@contact_account)
-                  %span.username= "@#{@contact_account.acct}"
+                = link_to TagManager.instance.url_for(@instance_presenter.contact_account) do
+                  %span.display_name.emojify= display_name(@instance_presenter.contact_account)
+                  %span.username= "@#{@instance_presenter.contact_account.acct}"
 
-          - unless @contact_email.blank?
+          - unless @instance_presenter.contact_email.blank?
             .contact-email
               = t 'about.business_email'
-              %strong= @contact_email
+              %strong= @instance_presenter.contact_email
       .panel
         .panel-header= t 'about.links'
         .panel-list
diff --git a/app/views/about/index.html.haml b/app/views/about/show.html.haml
index f6b0c1668..8a0d00daa 100644
--- a/app/views/about/index.html.haml
+++ b/app/views/about/show.html.haml
@@ -8,7 +8,7 @@
   %meta{ property: 'og:site_name', content: site_title }/
   %meta{ property: 'og:type', content: 'website' }/
   %meta{ property: 'og:title', content: Rails.configuration.x.local_domain }/
-  %meta{ property: 'og:description', content: @description.blank? ? "Mastodon is a free, open-source social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly" : strip_tags(@description) }/
+  %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.blank? ? t('about.about_mastodon') : @instance_presenter.site_description) }/
   %meta{ property: 'og:image', content: asset_url('mastodon_small.jpg') }/
   %meta{ property: 'og:image:width', content: '400' }/
   %meta{ property: 'og:image:height', content: '400' }/
@@ -24,28 +24,14 @@
   .screenshot-with-signup
     .mascot= image_tag 'fluffy-elephant-friend.png'
 
-    - if @open_registrations
-      = simple_form_for(@user, url: user_registration_path) do |f|
-        = f.simple_fields_for :account do |ff|
-          = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
-
-        = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
-        = f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
-        = f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') }
-
-        .actions
-          = f.button :button, t('about.get_started'), type: :submit
-
-        .info
-          = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
-          ·
-          = link_to t('about.about_this'), about_more_path
+    - if @instance_presenter.open_registrations
+      = render 'registration'
     - else
       .closed-registrations-message
-        - if @closed_registrations_message.blank?
+        - if @instance_presenter.closed_registrations_message.blank?
           %p= t('about.closed_registrations')
         - else
-          = @closed_registrations_message.html_safe
+          = @instance_presenter.closed_registrations_message.html_safe
         .info
           = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
           ·
@@ -85,9 +71,9 @@
           = fa_icon('li check-square')
           = t 'about.features.api'
 
-  - unless @description.blank?
+  - unless @instance_presenter.site_description.blank?
     %h3= t('about.description_headline', domain: Rails.configuration.x.local_domain)
-    %p= @description.html_safe
+    %p= @instance_presenter.site_description.html_safe
 
   .actions
     .info
diff --git a/app/views/accounts/followers.html.haml b/app/views/accounts/followers.html.haml
index 493491020..fa5071f38 100644
--- a/app/views/accounts/followers.html.haml
+++ b/app/views/accounts/followers.html.haml
@@ -9,4 +9,4 @@
   - else
     = render partial: 'grid_card', collection: @followers, as: :account, cached: true
 
-= will_paginate @followers, pagination_options
+= paginate @followers
diff --git a/app/views/accounts/following.html.haml b/app/views/accounts/following.html.haml
index 370cd6c48..987dcba1f 100644
--- a/app/views/accounts/following.html.haml
+++ b/app/views/accounts/following.html.haml
@@ -9,4 +9,4 @@
   - else
     = render partial: 'grid_card', collection: @following, as: :account, cached: true
 
-= will_paginate @following, pagination_options
+= paginate @following
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index e90897729..3b0d69dcd 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -31,4 +31,4 @@
 
   .pagination
     - if @statuses.size == 20
-      = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next_page', rel: 'next'
+      = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next', rel: 'next'
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index f8ed4ef97..4d636601e 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -46,4 +46,4 @@
           = table_link_to 'globe', 'Public', TagManager.instance.url_for(account)
           = table_link_to 'pencil', 'Edit', admin_account_path(account.id)
 
-= will_paginate @accounts, pagination_options
+= paginate @accounts
diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml
index eb7894b86..fe6ff683f 100644
--- a/app/views/admin/domain_blocks/index.html.haml
+++ b/app/views/admin/domain_blocks/index.html.haml
@@ -13,5 +13,5 @@
           %samp= block.domain
         %td= block.severity
 
-= will_paginate @blocks, pagination_options
+= paginate @blocks
 = link_to 'Add new', new_admin_domain_block_path, class: 'button'
diff --git a/app/views/admin/pubsubhubbub/index.html.haml b/app/views/admin/pubsubhubbub/index.html.haml
index cb11a502c..2b8e36e6a 100644
--- a/app/views/admin/pubsubhubbub/index.html.haml
+++ b/app/views/admin/pubsubhubbub/index.html.haml
@@ -26,4 +26,4 @@
           - else
             = l subscription.last_successful_delivery_at
 
-= will_paginate @subscriptions, pagination_options
+= paginate @subscriptions
diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml
index 839259dc2..9c5c78935 100644
--- a/app/views/admin/reports/index.html.haml
+++ b/app/views/admin/reports/index.html.haml
@@ -29,4 +29,4 @@
           %td= truncate(report.comment, length: 30, separator: ' ')
           %td= table_link_to 'circle', 'View', admin_report_path(report)
 
-= will_paginate @reports, pagination_options
+= paginate @reports
diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl
index 32df0457a..8826aa22d 100644
--- a/app/views/api/v1/accounts/show.rabl
+++ b/app/views/api/v1/accounts/show.rabl
@@ -4,8 +4,9 @@ attributes :id, :username, :acct, :display_name, :locked, :created_at
 
 node(:note)            { |account| Formatter.instance.simplified_format(account) }
 node(:url)             { |account| TagManager.instance.url_for(account) }
-node(:avatar)          { |account| full_asset_url(account.avatar.url(:original)) }
-node(:header)          { |account| full_asset_url(account.header.url(:original)) }
-node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : account.followers_count }
-node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : account.following_count }
-node(:statuses_count)  { |account| defined?(@statuses_counts_map)  ? (@statuses_counts_map[account.id]  || 0) : account.statuses_count }
+node(:avatar)          { |account| full_asset_url(account.avatar_original_url) }
+node(:avatar_static)   { |account| full_asset_url(account.avatar_static_url) }
+node(:header)          { |account| full_asset_url(account.header_original_url) }
+node(:header_static)   { |account| full_asset_url(account.header_static_url) }
+
+attributes :followers_count, :following_count, :statuses_count
diff --git a/app/views/kaminari/_next_page.html.haml b/app/views/kaminari/_next_page.html.haml
new file mode 100644
index 000000000..30a3643d6
--- /dev/null
+++ b/app/views/kaminari/_next_page.html.haml
@@ -0,0 +1,9 @@
+-#  Link to the "Next" page
+-#  available local variables
+-#    url:           url to the next page
+-#    current_page:  a page object for the currently displayed page
+-#    total_pages:   total number of pages
+-#    per_page:      number of items to fetch per page
+-#    remote:        data-remote
+%span.next
+  = link_to_unless current_page.last?, safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), url, rel: 'next', remote: remote
diff --git a/app/views/kaminari/_paginator.html.haml b/app/views/kaminari/_paginator.html.haml
new file mode 100644
index 000000000..b1da236d5
--- /dev/null
+++ b/app/views/kaminari/_paginator.html.haml
@@ -0,0 +1,16 @@
+-#  The container tag
+-#  available local variables
+-#    current_page:  a page object for the currently displayed page
+-#    total_pages:   total number of pages
+-#    per_page:      number of items to fetch per page
+-#    remote:        data-remote
+-#    paginator:     the paginator that renders the pagination tags inside
+= paginator.render do
+  %nav.pagination
+    = prev_page_tag unless current_page.first?
+    - each_page do |page|
+      - if page.display_tag?
+        = page_tag page
+      - elsif !page.was_truncated?
+        = gap_tag
+    = next_page_tag unless current_page.last?
diff --git a/app/views/kaminari/_prev_page.html.haml b/app/views/kaminari/_prev_page.html.haml
new file mode 100644
index 000000000..1089e3566
--- /dev/null
+++ b/app/views/kaminari/_prev_page.html.haml
@@ -0,0 +1,9 @@
+-#  Link to the "Previous" page
+-#  available local variables
+-#    url:           url to the previous page
+-#    current_page:  a page object for the currently displayed page
+-#    total_pages:   total number of pages
+-#    per_page:      number of items to fetch per page
+-#    remote:        data-remote
+%span.prev
+  = link_to_unless current_page.first?, safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '), url, rel: 'prev', remote: remote
diff --git a/app/views/shared/_landing_strip.html.haml b/app/views/shared/_landing_strip.html.haml
index bb081e544..3536c5ca8 100644
--- a/app/views/shared/_landing_strip.html.haml
+++ b/app/views/shared/_landing_strip.html.haml
@@ -1,2 +1,5 @@
 .landing-strip
-  = t('landing_strip_html', name: display_name(account), domain: Rails.configuration.x.local_domain, sign_up_path: new_user_registration_path)
+  = t('landing_strip_html',
+    name: content_tag(:span, display_name(account), class: :emojify),
+    domain: Rails.configuration.x.local_domain,
+    sign_up_path: new_user_registration_path)
diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml
index 434c5c8da..1333d4d82 100644
--- a/app/views/stream_entries/_status.html.haml
+++ b/app/views/stream_entries/_status.html.haml
@@ -13,7 +13,7 @@
         = fa_icon('retweet fw')
       %span
         = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do
-          %strong= display_name(status.account)
+          %strong.emojify= display_name(status.account)
         = t('stream_entries.reblogged')
 
   = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: status.proper }
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index 32a50e158..c894cdb2e 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -15,4 +15,4 @@
 
 - if @statuses.size == 20
   .pagination
-    = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next_page', rel: 'next'
+    = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next', rel: 'next'
diff --git a/app/views/user_mailer/confirmation_instructions.fr.html.erb b/app/views/user_mailer/confirmation_instructions.fr.html.erb
index 2665f1a20..6c45f1a21 100644
--- a/app/views/user_mailer/confirmation_instructions.fr.html.erb
+++ b/app/views/user_mailer/confirmation_instructions.fr.html.erb
@@ -1,5 +1,5 @@
 <p>Bienvenue <%= @resource.email %>&nbsp;!</p>
 
-<p>Vous pouvez confirmer l'email de votre compte Mastodon en cliquant sur le lien ci-dessous&nbsp;:</p>
+<p>Vous pouvez confirmer le courriel de votre compte Mastodon en cliquant sur le lien ci-dessous&nbsp;:</p>
 
 <p><%= link_to 'Confirmer mon compte', confirmation_url(@resource, confirmation_token: @token) %></p>
diff --git a/app/views/user_mailer/confirmation_instructions.fr.text.erb b/app/views/user_mailer/confirmation_instructions.fr.text.erb
index 9d33450f8..dfa3f9f7c 100644
--- a/app/views/user_mailer/confirmation_instructions.fr.text.erb
+++ b/app/views/user_mailer/confirmation_instructions.fr.text.erb
@@ -1,5 +1,5 @@
 Bienvenue <%= @resource.email %> !
 
-Vous pouvez confirmer l'email de votre compte Mastodon en cliquant sur le lien ci-dessous :
+Vous pouvez confirmer le courriel de votre compte Mastodon en cliquant sur le lien ci-dessous :
 
 <%= confirmation_url(@resource, confirmation_token: @token) %>
diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb
index d5a33cada..ad4f1b004 100644
--- a/app/workers/import_worker.rb
+++ b/app/workers/import_worker.rb
@@ -25,7 +25,7 @@ class ImportWorker
   def process_blocks(import)
     from_account = import.account
 
-    CSV.foreach(import.data.path) do |row|
+    CSV.new(open(import.data.url)).each do |row|
       next if row.size != 1
 
       begin
@@ -41,7 +41,7 @@ class ImportWorker
   def process_follows(import)
     from_account = import.account
 
-    CSV.foreach(import.data.path) do |row|
+    CSV.new(open(import.data.url)).each do |row|
       next if row.size != 1
 
       begin
diff --git a/config/application.rb b/config/application.rb
index 9a5c0d0d3..dc937ca0e 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -24,7 +24,9 @@ module Mastodon
 
     # The default locale is :en and 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, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi, :eo]
+
+    config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi, :eo, :ru]
+
     config.i18n.default_locale    = :en
 
     # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
diff --git a/config/database.yml b/config/database.yml
index 5ec342f93..810b83278 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -22,3 +22,4 @@ production:
   password: <%= ENV['DB_PASS'] || '' %>
   host: <%= ENV['DB_HOST'] || 'localhost' %>
   port: <%= ENV['DB_PORT'] || 5432 %>
+  prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %>
diff --git a/config/environments/production.rb b/config/environments/production.rb
index dc5dd4afd..05cced67b 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -38,9 +38,9 @@ Rails.application.configure do
   # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
   config.force_ssl = false
 
-  # Use the lowest log level to ensure availability of diagnostic information
+  # By default, use the lowest log level to ensure availability of diagnostic information
   # when problems arise.
-  config.log_level = :debug
+  config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'debug').to_sym
 
   # Prepend all log lines with the following tags.
   config.log_tags = [:request_id]
@@ -99,7 +99,9 @@ Rails.application.configure do
     :user_name      => ENV['SMTP_LOGIN'],
     :password       => ENV['SMTP_PASSWORD'],
     :domain         => ENV['SMTP_DOMAIN'] || config.x.local_domain,
-    :authentication => :plain,
+    :authentication => ENV['SMTP_AUTH_METHOD'] || :plain,
+    :openssl_verify_mode => ENV['SMTP_OPENSSL_VERIFY_MODE'] || 'peer',
+    :enable_starttls_auto => ENV['SMTP_ENABLE_STARTTLS_AUTO'] || true,
   }
 
   config.action_mailer.delivery_method = :smtp
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 4304bbd18..7ae143f93 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -33,7 +33,7 @@ search:
 
 ignore_unused:
   - 'activerecord.attributes.*'
-  - '{devise,will_paginate,doorkeeper}.*'
+  - '{devise,pagination,doorkeeper}.*'
   - '{datetime,time}.*'
   - 'simple_form.{yes,no}'
   - 'simple_form.{placeholders,hints,labels}.*'
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index ede6640bb..3c23e7b2e 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -74,7 +74,8 @@ Devise.setup do |config|
   # It will change confirmation, password recovery and other workflows
   # to behave the same regardless if the e-mail provided was right or wrong.
   # Does not affect registerable.
-  # config.paranoid = true
+  # See : https://github.com/plataformatec/devise/wiki/How-To:-Using-paranoid-mode,-avoid-user-enumeration-on-registerable
+  config.paranoid = true
 
   # By default Devise will store the user in session. You can skip storage for
   # particular strategies by setting this option.
diff --git a/config/initializers/httplog.rb b/config/initializers/httplog.rb
index 37f113d5d..5cfc16a8b 100644
--- a/config/initializers/httplog.rb
+++ b/config/initializers/httplog.rb
@@ -1,3 +1,5 @@
-HttpLog.options[:logger] = Rails.logger
-HttpLog.options[:color]  = { color: :yellow }
-HttpLog.options[:compact_log] = true
+HttpLog.configure do |config|
+  config.logger = Rails.logger
+  config.color = { color: :yellow }
+  config.compact_log = true
+end
diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb
new file mode 100644
index 000000000..bd455f382
--- /dev/null
+++ b/config/initializers/kaminari_config.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+Kaminari.configure do |config|
+  config.default_per_page = 40
+  config.window = 1
+  config.left = 3
+  config.right = 1
+end
diff --git a/config/initializers/pagination.rb b/config/initializers/pagination.rb
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/config/initializers/pagination.rb
diff --git a/config/locales/de.yml b/config/locales/de.yml
index ed54bb699..75ac4e1bb 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -88,5 +88,3 @@ de:
       default: "%d.%m.%Y %H:%M"
   users:
     invalid_email: Inkorrekte E-mail-Addresse
-  will_paginate:
-    page_gap: "&hellip;"
diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml
index ce44d041a..3b46b01e3 100644
--- a/config/locales/devise.fr.yml
+++ b/config/locales/devise.fr.yml
@@ -58,4 +58,4 @@ fr:
       not_locked: n'était pas verrouillé(e)
       not_saved:
         one: '1 erreur a empêché ce(tte) %{resource} d''être sauvegardé(e) :'
-        other: '%{count} erreurs ont empêché ce(tte) %{resource} d''être sauvegardé(e): '
+        other: '%{count} erreurs ont empêché ce(tte) %{resource} d''être sauvegardé(e) : '
diff --git a/config/locales/devise.ru.yml b/config/locales/devise.ru.yml
new file mode 100644
index 000000000..f829f9d8e
--- /dev/null
+++ b/config/locales/devise.ru.yml
@@ -0,0 +1,61 @@
+---
+ru:
+  devise:
+    confirmations:
+      confirmed: Ваш адрес e-mail был успешно подтвержден.
+      send_instructions: Вы получите e-mail с инструкцией по подтверждению Вашего адреса e-mail в течение нескольких минут.
+      send_paranoid_instructions: Если Ваш адрес e-mail есть в нашей базе данных, вы получите e-mail с инструкцией по подтверждению Вашего адреса в течение нескольких минут.
+    failure:
+      already_authenticated: Вы уже авторизованы.
+      inactive: Ваш аккаунт еще не активирован.
+      invalid: Неверно введены %{authentication_keys} или пароль.
+      last_attempt: У Вас есть последняя попытка, после чего вход будет заблокирован.
+      locked: Ваш аккаунт заблокирован.
+      not_found_in_database: Неверно введены %{authentication_keys} или пароль.
+      timeout: Ваша сессия истекла. Пожалуйста, войдите снова, чтобы продолжить.
+      unauthenticated: Вам необходимо войти или зарегистрироваться.
+      unconfirmed: Вам необходимо подтвердить ваш адрес e-mail для продолжения.
+    mailer:
+      confirmation_instructions:
+        subject: 'Mastodon: Инструкция по подтверждению'
+      password_change:
+        subject: 'Mastodon: Пароль изменен'
+      reset_password_instructions:
+        subject: 'Mastodon: Инструкция по сбросу пароля'
+      unlock_instructions:
+        subject: 'Mastodon: Инструкция по разблокировке'
+    omniauth_callbacks:
+      failure: Не получилось аутентифицировать Вас с помощью %{kind} по следующей причине - "%{reason}".
+      success: Аутентификация с помощью аккаунта %{kind} прошла успешно.
+    passwords:
+      no_token: Вы можете получить доступ к этой странице, только перейдя по ссылке в e-mail для сброса пароля. Если Вы действительно перешли по такой ссылке, пожалуйста, удостоверьтесь, что ссылка была введена полностью и без изменений.
+      send_instructions: Вы получите e-mail с инструкцией по сбросу пароля в течение нескольких минут.
+      send_paranoid_instructions: Если Ваш адрес e-mail есть в нашей базе данных, Вы получите e-mail со ссылкой для сброса пароля в течение нескольких минут.
+      updated: Ваш пароль был успешно изменен. Вход выполнен.
+      updated_not_active: Ваш пароль был успешно изменен.
+    registrations:
+      destroyed: До свидания! Ваш аккаунт был успешно удален. Мы надеемся скоро увидеть Вас снова.
+      signed_up: Добро пожаловать! Вы успешно зарегистрировались.
+      signed_up_but_inactive: Вы успешно зарегистрировались. Тем не менее, мы не можем авторизовать Вас, поскольку Ваш аккаунт еще не активирован.
+      signed_up_but_locked: Вы успешно зарегистрировались. Тем не менее, мы не можем авторизовать Вас, поскольку Ваш аккаунт заблокирован.
+      signed_up_but_unconfirmed: Сообщение со ссылкой для подтверждения было выслано на Ваш адрес e-mail. Пожалуйста, пройдите по ссылке для активации Вашего аккаунта.
+      update_needs_confirmation: Вы успешно обновили Ваш аккаунт, но нам нужно подтвердить ваш новый адрес e-mail. Пожалуйста, проверьте почту и пройдите по ссылке для подтверждения Вашего нового адреса.
+      updated: Ваш аккаунт был успешно обновлен.
+    sessions:
+      already_signed_out: Выход прошел успешно.
+      signed_in: Вход прошел успешно.
+      signed_out: Выход прошел успешно.
+    unlocks:
+      send_instructions: Вы получите e-mail с инструкцией по разблокировке Вашего аккаунта в течение нескольких минут.
+      send_paranoid_instructions: Если Ваш аккаунт существует, Вы получите e-mail с инструкцией по его разблокировке в течение нескольких минут.
+      unlocked: Ваш аккаунт был успешно разблокирован. пожалуйста, войдите для продолжения.
+  errors:
+    messages:
+      already_confirmed: уже подтвержден, пожалуйста, попробуйте войти
+      confirmation_period_expired: не был подтвержден в течение %{period}, пожалуйста, запросите новый
+      expired: истек, пожалуйста, запросите новый
+      not_found: не найден
+      not_locked: не был заблокирован
+      not_saved:
+        one: '1 ошибка помешала сохранению этого %{resource}:'
+        other: "%{count} ошибки помешали сохранению этого %{resource}:"
diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml
index be109df9c..cfc9083d7 100644
--- a/config/locales/doorkeeper.fr.yml
+++ b/config/locales/doorkeeper.fr.yml
@@ -23,11 +23,11 @@ fr:
         edit: Modifier
         submit: Envoyer
       confirmations:
-        destroy: Êtes-vous certain?
+        destroy: Êtes-vous certain ?
       edit:
         title: Modifier l'application
       form:
-        error: Oups! Vérifier votre formulaire pour des erreurs possibles
+        error: Oups ! Vérifier votre formulaire pour des erreurs possibles
       help:
         native_redirect_uri: Utiliser %{native_redirect_uri} pour les tests locaux
         redirect_uri: Utiliser une ligne par URL
@@ -54,7 +54,7 @@ fr:
         title: Une erreur est survenue
       new:
         able_to: Cette application pourra
-        prompt: Autoriser %{client_name} à utiliser votre compte?
+        prompt: Autoriser %{client_name} à utiliser votre compte ?
         title: Autorisation requise
       show:
         title: Code d'autorisation
@@ -109,5 +109,5 @@ fr:
         title: Autorisation OAuth requise
     scopes:
       follow: s’abonner, se désabonner, bloquer, et débloquer des comptes
-      read: lire les données de votre compte  
+      read: lire les données de votre compte
       write: poster en tant que vous
diff --git a/config/locales/doorkeeper.ru.yml b/config/locales/doorkeeper.ru.yml
new file mode 100644
index 000000000..8862936dc
--- /dev/null
+++ b/config/locales/doorkeeper.ru.yml
@@ -0,0 +1,113 @@
+---
+ru:
+  activerecord:
+    attributes:
+      doorkeeper/application:
+        name: Название
+        redirect_uri: URI перенаправления
+    errors:
+      models:
+        doorkeeper/application:
+          attributes:
+            redirect_uri:
+              fragment_present: не может содержать фрагмент.
+              invalid_uri: должен быть правильным URI.
+              relative_uri: должен быть абсолютным URI.
+              secured_uri: должен быть HTTPS/SSL URI.
+  doorkeeper:
+    applications:
+      buttons:
+        authorize: Авторизовать
+        cancel: Отменить
+        destroy: Удалить
+        edit: Изменить
+        submit: Принять
+      confirmations:
+        destroy: Вы уверены?
+      edit:
+        title: Изменить приложение
+      form:
+        error: Ой! Проверьте Вашу форму на возможные ошибки
+      help:
+        native_redirect_uri: Используйте %{native_redirect_uri} для локального тестирования
+        redirect_uri: Используйте по одной строке на URI
+        scopes: Разделяйте список разрешений пробелами. Оставьте незаполненным для использования разрешений по умолчанию.
+      index:
+        callback_url: Callback URL
+        name: Название
+        new: Новое Приложение
+        title: Ваши приложения
+      new:
+        title: Новое Приложение
+      show:
+        actions: Действия
+        application_id: Id приложения
+        callback_urls: Callback urls
+        scopes: Разрешения
+        secret: Секрет
+        title: 'Приложение: %{name}'
+    authorizations:
+      buttons:
+        authorize: Авторизовать
+        deny: Отказать
+      error:
+        title: Произошла ошибка
+      new:
+        able_to: Оно сможет
+        prompt: Приложение %{client_name} запрашивает доступ к Вашему аккаунту
+        title: Требуется авторизация
+      show:
+        title: Код авторизации
+    authorized_applications:
+      buttons:
+        revoke: Отозвать авторизацию
+      confirmations:
+        revoke: Вы уверены?
+      index:
+        application: Приложение
+        created_at: Авторизовано
+        date_format: "%Y-%m-%d %H:%M:%S"
+        scopes: Разрешения
+        title: Ваши авторизованные приложения
+    errors:
+      messages:
+        access_denied: Владелец ресурса или сервер авторизации ответил отказом на Ваш запрос.
+        credential_flow_not_configured: Поток с предоставлением клиенту пароля завершился неудачей, поскольку параметр Doorkeeper.configure.resource_owner_from_credentials не был сконфигурирован.
+        invalid_client: Клиентская аутентификация завершилась неудачей (неизвестный клиент, не включена клиентская аутентификация, или метод аутентификации не поддерживается.
+        invalid_grant: Предоставленный доступ некорректен, истек, отозван, не совпадает с URI перенаправления, использованным в запросе авторизации, или был выпущен для другого клиента.
+        invalid_redirect_uri: Включенный URI перенаправления некорректен.
+        invalid_request: В запросе не хватает обязательного параметра, присутствует неподдерживаемое значение параметра, либо он был сформирован неверно.
+        invalid_resource_owner: Предоставленные данные владельца ресурса некорректны, или владелец ресурса не может быть найден
+        invalid_scope: Запрошенное разрешение некорректно, неизвестно или неверно сформировано.
+        invalid_token:
+          expired: Токен доступа истек
+          revoked: Токен доступа был отменен
+          unknown: Токен доступа некорректен
+        resource_owner_authenticator_not_configured: Поиск владельца ресурса завершился неудачей, поскольку параметр Doorkeeper.configure.resource_owner_authenticator не был сконфигурирован.
+        server_error: Сервер авторизации встретился с неожиданной ошибкой, не позволившей ему выполнить запрос.
+        temporarily_unavailable: Сервер авторизации в данный момент не может выполнить запрос по причине временной перегрузки или профилактики.
+        unauthorized_client: Клиент не авторизован для выполнения этого запроса с использованием этого метода.
+        unsupported_grant_type: Тип авторизации не поддерживается сервером авторизации.
+        unsupported_response_type: Сервер авторизации не поддерживает этот тип ответа.
+    flash:
+      applications:
+        create:
+          notice: Приложение создано.
+        destroy:
+          notice: Приложение удалено.
+        update:
+          notice: Приложение обновлено.
+      authorized_applications:
+        destroy:
+          notice: Авторизация приложения отозвана.
+    layouts:
+      admin:
+        nav:
+          applications: Приложения
+          oauth2_provider: Провайдер OAuth2
+      application:
+        title: Требуется авторизация OAuth
+    scopes:
+      follow: подписываться, отписываться, блокировать и разблокировать аккаунты
+      read: читать данные Вашего аккаунта
+      write: отправлять за Вас посты
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 118798ba1..6c4738991 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -126,6 +126,7 @@ en:
   pagination:
     next: Next
     prev: Prev
+    truncate: "&hellip;"
   remote_follow:
     acct: Enter your username@domain you want to follow from
     missing_resource: Could not find the required redirect URL for your account
@@ -169,5 +170,3 @@ en:
   users:
     invalid_email: The e-mail address is invalid
     invalid_otp_token: Invalid two-factor code
-  will_paginate:
-    page_gap: "&hellip;"
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index 3644b37bb..e82e42495 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -160,5 +160,3 @@ eo:
   users:
     invalid_email: La retpoŝt-adreso ne estas valida
     invalid_otp_token: La dufaktora aŭtentigila kodo ne estas valida
-  will_paginate:
-    page_gap: "&hellip;"
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 19f2c71b8..42245d675 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -51,5 +51,3 @@ es:
   settings:
     edit_profile: Editar perfil
     preferences: Preferencias
-  will_paginate:
-    page_gap: "&hellip;"
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 56aa9df49..c11237226 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -160,5 +160,3 @@ fi:
   users:
     invalid_email: Virheellinen sähköposti
     invalid_otp_token: Virheellinen kaksivaihe tunnistus koodi
-  will_paginate:
-    page_gap: "&hellip;"
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 9727f3b7e..92cf43944 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -4,7 +4,7 @@ fr:
     about_mastodon: Mastodon est un serveur <em>libre</em> de réseautage social. Alternative <em>décentralisée</em> aux plateformes commerciales, la monopolisation de vos communications par une entreprise unique est évitée. Tout un chacun peut faire tourner Mastodon et participer au <em>réseau social</em> de manière transparente.
     about_this: À propos de cette instance
     apps: Applications
-    business_email: E-mail professionnel
+    business_email: Courriel professionnel
     closed_registrations: Les inscriptions sont actuellement fermées sur cette instance.
     contact: Contact
     description_headline: Qu'est-ce que %{domain} ?
@@ -40,9 +40,9 @@ fr:
     remote_follow: Suivre à distance
     unfollow: Ne plus suivre
   application_mailer:
-    settings: 'Changer les préférences e-mail: ${link}'
+    settings: 'Changer les préférences courriel : ${link}'
     signature: Notifications de Mastodon depuis %{instance}
-    view: 'Voir:'
+    view: 'Voir :'
   applications:
     invalid_url: L'URL fournie est invalide
   auth:
@@ -58,21 +58,27 @@ fr:
   authorize_follow:
     error: Malheureusement, il y a eu une erreur en cherchant les détails du compte distant
     follow: Suivre
-    prompt_html: 'Vous (<strong>%{self}</strong>) avez demandé à suivre:'
+    prompt_html: 'Vous (<strong>%{self}</strong>) avez demandé à suivre :'
     title: Suivre %{acct}
   datetime:
     distance_in_words:
       about_x_hours: "%{count}h"
-      about_x_months: "%{count}mo"
-      about_x_years: "%{count}y"
-      almost_x_years: "%{count}y"
+      about_x_months: "%{count}mois"
+      about_x_years:
+        one: un an
+        other: "%{count} ans"
+      almost_x_years:
+        one: un an
+        other: "%{count} ans"
       half_a_minute: A l'instant
-      less_than_x_minutes: "%{count}m"
+      less_than_x_minutes: "%{count}min"
       less_than_x_seconds: A l'instant
-      over_x_years: "%{count}y"
-      x_days: "%{count}d"
-      x_minutes: "%{count}m"
-      x_months: "%{count}mo"
+      over_x_years:
+        one: un an
+        other: "%{count} ans"
+      x_days: "%{count}j"
+      x_minutes: "%{count}min"
+      x_months: "%{count}mois"
       x_seconds: "%{count}s"
   exports:
     blocks: Vous bloquez
@@ -96,7 +102,7 @@ fr:
   landing_strip_html: <strong>%{name}</strong> utilise <strong>%{domain}</strong>. Vous pouvez le/la suivre et interagir si vous possédez un compte quelque part dans le "fediverse". Si ce n'est pas le cas, vous pouvez <a href="%{sign_up_path}">en créer un ici</a>.
   notification_mailer:
     digest:
-      body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}):'
+      body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}) :'
       mention: '%{name} vous a mentionné⋅e'
       new_followers_summary:
         one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi !
@@ -156,10 +162,8 @@ fr:
     disable: Désactiver
     enable: Activer
     instructions_html: "<strong>Scannez ce QR code grâce à Google Authenticator, Authy ou une application similaire sur votre téléphone</strong>. Désormais, cette application générera des jetons que vous devrez saisir à chaque connexion."
-    plaintext_secret_html: 'Code secret en clair: <samp>%{secret}</samp>'
+    plaintext_secret_html: 'Code secret en clair : <samp>%{secret}</samp>'
     warning: Si vous ne pouvez pas configurer une application d'authentification maintenant, vous devriez cliquer sur "Désactiver" pour ne pas bloquer l'accès à votre compte.
   users:
-    invalid_email: L'adresse e-mail est invalide
+    invalid_email: L'adresse courriel est invalide
     invalid_otp_token: Le code d'authentification à deux facteurs est invalide
-  will_paginate:
-    page_gap: "&hellip;"
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 915d02c19..96b73d43c 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -51,5 +51,3 @@ hu:
   settings:
     edit_profile: Profil szerkesztése
     preferences: Beállítások
-  will_paginate:
-    page_gap: "&hellip;"
diff --git a/config/locales/no.yml b/config/locales/no.yml
index b9a752d5a..9aa966d2a 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -160,5 +160,3 @@
   users:
     invalid_email: E-post addressen er ugyldig
     invalid_otp_token: Ugyldig two-faktor kode
-  will_paginate:
-    page_gap: "&hellip;"
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index ad7d05e3b..f2c7458f7 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -51,5 +51,3 @@ pt:
   settings:
     edit_profile: Editar perfil
     preferences: Preferências
-  will_paginate:
-    page_gap: "&hellip;"
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
new file mode 100644
index 000000000..fab178629
--- /dev/null
+++ b/config/locales/ru.yml
@@ -0,0 +1,163 @@
+---
+ru:
+  about:
+    about_mastodon: Mastodon - это <em>свободная</em> социальная сеть с <em>открытым исходным кодом</em>. Как <em>децентрализованная</em> альтернатива коммерческим платформам, Mastodon предотвращает риск монополизации Вашего общения одной компанией. Выберите сервер, которому Вы доверяете &mdash; что бы Вы ни выбрали, Вы сможете общаться со всеми остальными. Любой может запустить свой собственный узел Mastodon и участвовать в <em>социальной сети</em> совершенно бесшовно.
+    about_this: Об этом узле
+    apps: Приложения
+    business_email: 'Деловой e-mail:'
+    closed_registrations: В данный момент регистрация на этом узле закрыта.
+    contact: Связаться
+    description_headline: Что такое %{domain}?
+    domain_count_after: другими узлами
+    domain_count_before: Связывается с
+    features:
+      api: Открытый API для приложений и сервисов
+      blocks: Продвинутые инструменты блокирования и глушения
+      characters: 500 символов на пост
+      chronology: Хронологические ленты
+      ethics: 'Этичный дизайн: нет рекламы, нет слежения'
+      gifv: GIFV и короткие видео
+      privacy: Тонкие настройки приватности для каждого поста
+      public: Публичные ленты
+    features_headline: Что выделяет Mastodon
+    get_started: Начать
+    links: Ссылки
+    other_instances: Другие узлы
+    source_code: Исходный код
+    status_count_after: статусов
+    status_count_before: Автор
+    terms: Условия
+    user_count_after: пользователей
+    user_count_before: Здесь живет
+  accounts:
+    follow: Подписаться
+    followers: Подписчики
+    following: Подписан(а)
+    nothing_here: Здесь ничего нет!
+    people_followed_by: Люди, на которых подписан(а) %{name}
+    people_who_follow: Подписчики %{name}
+    posts: Посты
+    remote_follow: Подписаться на удаленном узле
+    unfollow: Отписаться
+  application_mailer:
+    settings: 'Изменить настройки e-mail: %{link}'
+    signature: Уведомления Mastodon от %{instance}
+    view: 'View:'
+  applications:
+    invalid_url: Введенный URL неверен
+  auth:
+    change_password: Изменить пароль
+    didnt_get_confirmation: Не получили инструкцию для подтверждения?
+    forgot_password: Забыли пароль?
+    login: Войти
+    logout: Выйти
+    register: Зарегистрироваться
+    resend_confirmation: Повторить отправку инструкции для подтверждения
+    reset_password: Сбросить пароль
+    set_new_password: Задать новый пароль
+  authorize_follow:
+    error: К сожалению, при поиске удаленного аккаунта возникла ошибка
+    follow: Подписаться
+    prompt_html: 'Вы (<strong>%{self}</strong>) запросили подписку:'
+    title: Подписаться на %{acct}
+  datetime:
+    distance_in_words:
+      about_x_hours: "%{count}ч"
+      about_x_months: "%{count}мес"
+      about_x_years: "%{count}г"
+      almost_x_years: "%{count}г"
+      half_a_minute: Только что
+      less_than_x_minutes: "%{count}мин"
+      less_than_x_seconds: Только что
+      over_x_years: "%{count}г"
+      x_days: "%{count}д"
+      x_minutes: "%{count}мин"
+      x_months: "%{count}мес"
+      x_seconds: "%{count}сек"
+  exports:
+    blocks: Вы заблокировали
+    csv: CSV
+    follows: Подписки
+    storage: Ваш медиаконтент
+  generic:
+    changes_saved_msg: Изменения успешно сохранены!
+    powered_by: работает на %{link}
+    save_changes: Сохранить изменения
+    validation_errors:
+      one: Что-то здесь не так! Пожалуйста, прочитайте об ошибке ниже
+      other: Что-то здесь не так! Пожалуйста, прочитайте о %{count} ошибках ниже
+  imports:
+    preface: Вы можете загрузить некоторые данные, например, списки людей, на которых Вы подписаны или которых блокируете, в Ваш аккаунт на этом узле из файлов, экспортированных с другого узла.
+    success: Ваши данные были успешно загружены и будут обработаны с должной скоростью
+    types:
+      blocking: Список блокируемых
+      following: Список подписок
+    upload: Загрузить
+  landing_strip_html: <strong>%{name}</strong> - пользователь на <strong>%{domain}</strong>. Вы можете подписаться на него/нее и общаться с ним/ней, если у Вас есть аккаунт на любом узле общей сети. Если у Вас его нет, вы можете <a href="%{sign_up_path}">зарегистрироваться здесь</a>.
+  notification_mailer:
+    digest:
+      body: 'Кратко о пропущенном Вами на %{instance} с Вашего последнего захода %{since}:'
+      mention: "%{name} упомянул(а) Вас в:"
+      new_followers_summary:
+        one: У Вас появился новый подписчик! Ура!
+        other: У Вас появилось %{count} новых подписчика(-ов)! Отлично!
+      subject:
+        one: "1 новое уведомление с Вашего последнего захода \U0001F418"
+        other: "%{count} новых уведомлений с Вашего последнего захода \U0001F418"
+    favourite:
+      body: 'Ваш статус понравился %{name}:'
+      subject: "%{name} понравился Ваш статус"
+    follow:
+      body: "%{name} теперь подписан(а) на Вас!"
+      subject: "%{name} теперь подписан(а) на Вас"
+    follow_request:
+      body: "%{name} запросил Вас о подписке"
+      subject: '%{name} хочет подписаться на Вас'
+    mention:
+      body: 'Вас упомянул(а) %{name} в:'
+      subject: Вы были упомянуты %{name}
+    reblog:
+      body: 'Ваш статус был продвинут %{name}:'
+      subject: "%{name} продвинул(а) Ваш статус"
+  pagination:
+    next: След
+    prev: Пред
+  remote_follow:
+    acct: Введите username@domain, откуда Вы хотите подписаться
+    missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей
+    proceed: Продолжить подписку
+    prompt: 'Вы ходите подписаться на:'
+  settings:
+    authorized_apps: Авторизованные приложения
+    back: Назад в Mastodon
+    edit_profile: Изменить профиль
+    export: Экспорт данных
+    import: Импорт
+    preferences: Настройки
+    settings: Опции
+    two_factor_auth: Двухфакторная аутентификация
+  statuses:
+    open_in_web: Открыть в WWW
+    over_character_limit: превышен лимит символов (%{max})
+    show_more: Подробнее
+    visibilities:
+      private: Показывать только подписчикам
+      public: Публичный
+      unlisted: Публичный, но без отображения в публичных лентах
+  stream_entries:
+    click_to_show: Показать
+    reblogged: продвинул(а)
+    sensitive_content: Чувствительный контент
+  time:
+    formats:
+      default: "%b %d, %Y, %H:%M"
+  two_factor_auth:
+    description_html: При включении <strong>двухфакторной аутентификации</strong>, вход потребует от Вас использования Вашего телефона, который сгенерирует входные токены.
+    disable: Отключить
+    enable: Включить
+    instructions_html: "<strong>Отсканируйте этот QR-код с помощью Google Authenticator или другого подобного приложения на Вашем телефоне</strong>. С этого момента приложение будет генерировать токены, которые будет необходимо ввести для входа."
+    plaintext_secret_html: 'Секрет открытым текстом: <samp>%{secret}</samp>'
+    warning: Если сейчас у Вас не получается настроить аутентификатор, нажмите "отключить", иначе Вы не сможете войти!
+  users:
+    invalid_email: Введенный e-mail неверен
+    invalid_otp_token: Введен неверный код
diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml
new file mode 100644
index 000000000..6f4873bfd
--- /dev/null
+++ b/config/locales/simple_form.ru.yml
@@ -0,0 +1,46 @@
+---
+ru:
+  simple_form:
+    hints:
+      defaults:
+        avatar: PNG, GIF или JPG. Максимально 2MB. Будет уменьшено до 120x120px
+        display_name: Максимально 30 символов
+        header: PNG, GIF или JPG. Максимально 2MB. Будет уменьшено до 700x335px
+        locked: Потребует от Вас ручного подтверждения подписчиков, изменит приватность постов по умолчанию на "только для подписчиков"
+        note: Максимально 160 символов
+      imports:
+        data: Файл CSV, экспортированный с другого узла Mastodon
+    labels:
+      defaults:
+        avatar: Аватар
+        confirm_new_password: Повторите новый пароль
+        confirm_password: Повторите пароль
+        current_password: Текущий пароль
+        data: Данные
+        display_name: Показываемое имя
+        email: Адрес e-mail
+        header: Заголовок
+        locale: Язык
+        locked: Сделать аккаунт приватным
+        new_password: Новый пароль
+        note: О Вас
+        otp_attempt: Двухфакторный код
+        password: Пароль
+        setting_default_privacy: Приватность постов
+        type: Тип импорта
+        username: Имя пользователя
+      interactions:
+        must_be_follower: Заблокировать уведомления не от подписчиков
+        must_be_following: Заблокировать уведомления от людей, на которых Вы не подписаны
+      notification_emails:
+        digest: Присылать дайджест по e-mail
+        favourite: Уведомлять по e-mail, когда кому-то нравится Ваш статус
+        follow: Уведомлять по e-mail, когда кто-то подписался на Вас
+        follow_request: Уведомлять по e-mail, когда кто-то запрашивает разрешение на подписку
+        mention: Уведомлять по e-mail, когда кто-то упомянул Вас
+        reblog: Уведомлять по e-mail, когда кто-то продвинул Ваш статус
+    'no': 'Нет'
+    required:
+      mark: "*"
+      text: обязательно
+    'yes': 'Да'
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 27e8135df..f7176e86d 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -51,5 +51,3 @@ uk:
   settings:
     edit_profile: Редагувати профіль
     preferences: Налаштування
-  will_paginate:
-    page_gap: "&hellip;"
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 78c4d46e2..48028d00c 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -150,5 +150,3 @@ zh-CN:
   users:
     invalid_email: 无效的邮箱
     invalid_otp_token: 无效的两步验证码
-  will_paginate:
-    page_gap: "&hellip;"
diff --git a/config/routes.rb b/config/routes.rb
index 9cbecf077..9adcdb862 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -163,6 +163,7 @@ Rails.application.routes.draw do
         collection do
           get :relationships
           get :verify_credentials
+          patch :update_credentials
           get :search
         end
 
@@ -188,11 +189,14 @@ Rails.application.routes.draw do
 
   get '/web/(*any)', to: 'home#index', as: :web
 
-  get '/about',      to: 'about#index'
+  get '/about',      to: 'about#show'
   get '/about/more', to: 'about#more'
   get '/terms',      to: 'about#terms'
 
   root 'home#index'
 
-  match '*unmatched_route', via: :all, to: 'application#raise_not_found'
+  match '*unmatched_route',
+    via: :all,
+    to: 'application#raise_not_found',
+    format: false
 end
diff --git a/config/settings.yml b/config/settings.yml
index e4501e6e6..d364120db 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -1,4 +1,11 @@
 # config/app.yml for rails-settings-cached
+#
+# This file contains default values, and does not need to be edited
+# when configuring an instance.  These settings may be changed by an
+# Administrator using the Web UI.
+#
+# For more information, see docs/Running-Mastodon/Administration-guide.md
+#
 defaults: &defaults
   site_title: 'Mastodon'
   site_description: ''
diff --git a/docker-compose.yml b/docker-compose.yml
index d6ba66dde..910bf8cfe 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,11 +1,20 @@
 version: '2'
 services:
+
   db:
     restart: always
     image: postgres:alpine
+### Uncomment to enable DB persistance
+#    volumes:
+#      - ./postgres:/var/lib/postgresql/data
+
   redis:
     restart: always
     image: redis:alpine
+### Uncomment to enable REDIS persistance
+#    volumes:
+#      - ./redis:/data
+
   web:
     restart: always
     build: .
@@ -19,6 +28,7 @@ services:
     volumes:
       - ./public/assets:/mastodon/public/assets
       - ./public/system:/mastodon/public/system
+
   streaming:
     restart: always
     build: .
@@ -29,6 +39,7 @@ services:
     depends_on:
       - db
       - redis
+
   sidekiq:
     restart: always
     build: .
diff --git a/docs/Running-Mastodon/Administration-guide.md b/docs/Running-Mastodon/Administration-guide.md
index 09b0f1df1..8bcfe7c9e 100644
--- a/docs/Running-Mastodon/Administration-guide.md
+++ b/docs/Running-Mastodon/Administration-guide.md
@@ -35,3 +35,11 @@ You are able to set the following settings:
 - Site extended description
 
 You may wish to use the extended description (shown at https://yourmastodon.instance/about/more ) to display content guidelines or a user agreement (see https://mastodon.social/about/more for an example).
+
+## Confirming Users Manually
+
+The following rake task:
+
+    RAILS_ENV=production bundle exec rails mastodon:confirm_email USER_EMAIL=alice@alice.com
+
+Will confirm a user manually, in case they don't have access to their confirmation email for whatever reason.
diff --git a/docs/Running-Mastodon/Production-guide.md b/docs/Running-Mastodon/Production-guide.md
index 785826aca..49f3e59b2 100644
--- a/docs/Running-Mastodon/Production-guide.md
+++ b/docs/Running-Mastodon/Production-guide.md
@@ -34,10 +34,19 @@ server {
   keepalive_timeout    70;
   sendfile             on;
   client_max_body_size 0;
-  gzip off;
 
   root /home/mastodon/live/public;
 
+  gzip on;
+  gzip_disable "msie6";
+  gzip_vary on;
+  gzip_proxied any;
+  gzip_comp_level 6;
+  gzip_buffers 16 8k;
+  gzip_http_version 1.1;
+  gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+
+
   add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
 
   location / {
@@ -49,7 +58,7 @@ server {
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Forwarded-Proto https;
-
+    proxy_set_header Proxy "";
     proxy_pass_header Server;
 
     proxy_pass http://localhost:3000;
@@ -67,6 +76,7 @@ server {
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Forwarded-Proto https;
+    proxy_set_header Proxy "";
 
     proxy_pass http://localhost:4000;
     proxy_buffering off;
@@ -121,7 +131,7 @@ It is recommended to use rbenv (exclusively from the `mastodon` user) to install
 [2]: https://github.com/rbenv/ruby-build#installation
 [3]: https://github.com/rbenv/ruby-build/wiki#suggested-build-environment
 
-Then once `rbenv` is ready, run `rbenv install 2.3.1` to install the Ruby version for Mastodon.
+Then once `rbenv` is ready, run `rbenv install 2.4.1` to install the Ruby version for Mastodon.
 
 ## Git
 
diff --git a/docs/Using-Mastodon/Apps.md b/docs/Using-Mastodon/Apps.md
index b5e1fa36b..ce3f2f1fc 100644
--- a/docs/Using-Mastodon/Apps.md
+++ b/docs/Using-Mastodon/Apps.md
@@ -14,5 +14,6 @@ Some people have started working on apps for the Mastodon API. Here is a list of
 |Tooter|Chrome|<https://github.com/ineffyble/tooter>|[@effy@mastodon.social](https://mastodon.social/users/effy)|
 |tootstream|CLI|<https://github.com/magicalraccoon/tootstream>|[@Raccoon@mastodon.social](https://mastodon.social/users/Raccoon)|
 |HackerNewsBot|CLI|<https://github.com/raymestalez/mastodon-hnbot>|[@rayalez@hackertribe.io](https://hackertribe.io/users/rayalez)|
+|Mastodon.tools|Wordpress, web browser, social network|<https://github.com/davidlibeau/mastodon-tools>|[@David@mastodon.xyz](https://mastodon.xyz/users/David)|
 
 If you have a project like this, let me know so I can add it to the list!
diff --git a/docs/Using-Mastodon/List-of-Mastodon-instances.md b/docs/Using-Mastodon/List-of-Mastodon-instances.md
index db35edb1a..49b2c2012 100644
--- a/docs/Using-Mastodon/List-of-Mastodon-instances.md
+++ b/docs/Using-Mastodon/List-of-Mastodon-instances.md
@@ -76,7 +76,7 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
 | [mastodon.fun](https://mastodon.fun/)|Mastodon for everyone ! |Yes|Yes|
 | [oulipo.social](https://oulipo.social/)|An Oulipo Mastodon in which that fifth symbol in Latin script is taboo|Yes|No|
 | [indigo.zone](https://indigo.zone)|Open Registrations, General Purpose|Yes|No|
+| [mastodon.cloud](https://mastodon.cloud)|An open Mastodon instance with people from all around the world|Yes|Yes|
 | [mst3k.interlinked.me](https://mst3k.interlinked.me)|Open registrations, general purpose|Yes|Yes|
 
-
 We are no longer maintaining this list as instances are popping up too quickly for using GitHub to be a tenable system for tracking them. Please standby while we work on another solution
diff --git a/docs/Using-the-API/API.md b/docs/Using-the-API/API.md
index 8a648b6d0..fee1fde94 100644
--- a/docs/Using-the-API/API.md
+++ b/docs/Using-the-API/API.md
@@ -30,7 +30,7 @@ API overview
   - [Instance](#instance)
   - [Mention](#mention)
   - [Notification](#notification)
-  - [Relationships](#relationships)
+  - [Relationship](#relationship)
   - [Results](#results)
   - [Status](#status)
   - [Tag](#tag)
@@ -85,6 +85,17 @@ Returns an [Account](#account).
 
 Returns the authenticated user's [Account](#account).
 
+#### Updating the current user:
+
+    PATCH /api/v1/accounts/update_credentials
+
+Form data:
+
+- `display_name`: The name to display in the user's profile
+- `note`: A new biography for the user
+- `avatar`: A base64 encoded image to display as the user's avatar (e.g. `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADrCAYAAAA...`)
+- `header`: A base64 encoded image to display as the user's header image (e.g. `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADrCAYAAAA...`)
+
 #### Getting an account's followers:
 
     GET /api/v1/accounts/:id/followers
@@ -351,15 +362,15 @@ Returns an empty object.
 
 #### Reblogging/unreblogging a status:
 
-    POST /api/vi/statuses/:id/reblog
-    POST /api/vi/statuses/:id/unreblog
+    POST /api/v1/statuses/:id/reblog
+    POST /api/v1/statuses/:id/unreblog
 
 Returns the target [Status](#status).
 
 #### Favouriting/unfavouriting a status:
 
-    POST /api/vi/statuses/:id/favourite
-    POST /api/vi/statuses/:id/unfavourite
+    POST /api/v1/statuses/:id/favourite
+    POST /api/v1/statuses/:id/unfavourite
 
 Returns the target [Status](#status).
 
@@ -456,7 +467,7 @@ ___
 | `acct`                   | Equals `username` for local users, includes `@domain` for remote ones |
 | `id`                     | Account ID |
 
-### Notifications
+### Notification
 
 | Attribute                | Description |
 | ------------------------ | ----------- |
@@ -464,9 +475,9 @@ ___
 | `type`                   | One of: "mention", "reblog", "favourite", "follow" |
 | `created_at`             | The time the notification was created |
 | `account`                | The [Account](#account) sending the notification to the user |
-| `status`                 | The [Status](#status) associated with the notification, if applicible |
+| `status`                 | The [Status](#status) associated with the notification, if applicable |
 
-### Relationships
+### Relationship
 
 | Attribute                | Description |
 | ------------------------ | ----------- |
@@ -516,7 +527,7 @@ ___
 | `tags`                   | An array of [Tags](#tag) |
 | `application`            | [Application](#application) from which the status was posted |
 
-### Tags
+### Tag
 
 | Attribute                | Description |
 | ------------------------ | ----------- |
diff --git a/docs/Using-the-API/OAuth-details.md b/docs/Using-the-API/OAuth-details.md
index d0b5abd40..e88a25682 100644
--- a/docs/Using-the-API/OAuth-details.md
+++ b/docs/Using-the-API/OAuth-details.md
@@ -9,4 +9,4 @@ The API is divided up into access scopes:
 - `write`: Post statuses and upload media for statuses
 - `follow`: Follow, unfollow, block, unblock
 
-Multiple scopes can be requested during the authorization phase with the `scope` query param (space-separate the scopes).
+Multiple scopes can be requested during the authorization phase with the `scope` query param (space-separate the scopes). If you do not specify a `scope` in your authorization request, the resulting access token will default to `read` access.
diff --git a/docs/Using-the-API/Testing-with-cURL.md b/docs/Using-the-API/Testing-with-cURL.md
index dc5f2022d..a373ec2bb 100644
--- a/docs/Using-the-API/Testing-with-cURL.md
+++ b/docs/Using-the-API/Testing-with-cURL.md
@@ -3,7 +3,7 @@ Testing the API with cURL
 
 Mastodon builds around the idea of being a server first, rather than a client itself. Similarly to how a XMPP chat server communicates with others and with its own clients, Mastodon takes care of federation to other networks, like other Mastodon or GNU Social instances. So Mastodon provides a REST API, and a 3rd-party app system for using it via OAuth2.
 
-You can get a client ID and client secret required for OAuth [via an API end-point](API.md#oauth-apps).
+You can get a client ID and client secret required for OAuth [via an API end-point](API.md#apps).
 
 From these two, you will need to acquire an access token. It is possible to do using your account's e-mail and password like this:
 
@@ -13,6 +13,6 @@ The `/oauth/token` path will attempt to login with the given credentials, and th
 
 Use that token in any API requests by setting a header like this:
 
-    curl --header "Authorization: Bearer ACCESS_TOKEN_HERE" -sS https://mastodon.social/api/statuses/home
+    curl --header "Authorization: Bearer ACCESS_TOKEN_HERE" -sS https://mastodon.social/api/v1/timelines/home
 
 Please note that the password-based approach is not recommended especially if you're dealing with other user's accounts and not just your own. Usually you would use the authorization grant approach where you redirect the user to a web page on the original site where they can login and authorize the application and are then redirected back to your application with an access code.
diff --git a/docs/Using-the-API/Tips-for-app-developers.md b/docs/Using-the-API/Tips-for-app-developers.md
index 561f1e273..d60b472e5 100644
--- a/docs/Using-the-API/Tips-for-app-developers.md
+++ b/docs/Using-the-API/Tips-for-app-developers.md
@@ -13,4 +13,4 @@ Make sure that you make it possible to see the `acct` of any user in your app (s
 
 ## Formatting
 
-The API delivers already formatted HTML to your app. This isn't ideal since not all apps are based on HTML, but this is not fixable as its part of the way OStatus federation works. Most importantly, you get some information on linked entities alongside the HTML of the status body. For example, you get a list of mentioned users, and a list of media attachments, and a list of hashtags. It is possible to convert the HTML to whatever you need in your app by parsing the HTML tags and matching their `href`s to the linked entities. If a match cannot be found, the link must stay a clickable link.
+The API delivers already formatted HTML to your app. This isn't ideal since not all apps are based on HTML, but this is not fixable as it's part of the way OStatus federation works. Most importantly, you get some information on linked entities alongside the HTML of the status body. For example, you get a list of mentioned users, and a list of media attachments, and a list of hashtags. It is possible to convert the HTML to whatever you need in your app by parsing the HTML tags and matching their `href`s to the linked entities. If a match cannot be found, the link must stay a clickable link.
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 79dcb722a..a8fb58b7f 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -10,6 +10,18 @@ namespace :mastodon do
     puts "Congrats! #{user.account.username} is now an admin. \\o/\nNavigate to #{admin_settings_url} to get started"
   end
 
+  desc 'Manually confirms a user with associated user email address stored in USER_EMAIL environment variable.'
+  task confirm_email: :environment do
+    email = ENV.fetch('USER_EMAIL')
+    user = User.where(email: email).first
+    if user
+      user.update(confirmed_at: Time.now.utc)
+      puts "User #{email} confirmed."
+    else
+      abort "User #{email} not found."
+    end
+  end
+
   namespace :media do
     desc 'Removes media attachments that have not been assigned to any status for longer than a day'
     task clear: :environment do
@@ -80,5 +92,17 @@ namespace :mastodon do
 
       Rails.logger.debug 'Done!'
     end
+
+    desc 'Generate static versions of GIF avatars/headers'
+    task add_static_avatars: :environment do
+      Rails.logger.debug 'Generating static avatars/headers for GIF ones...'
+
+      Account.unscoped.where(avatar_content_type: 'image/gif').or(Account.unscoped.where(header_content_type: 'image/gif')).find_each do |account|
+        account.avatar.reprocess!
+        account.header.reprocess!
+      end
+
+      Rails.logger.debug 'Done!'
+    end
   end
 end
diff --git a/package.json b/package.json
index 14c8abe79..fee78dd69 100644
--- a/package.json
+++ b/package.json
@@ -72,5 +72,10 @@
     "webpack": "^2.2.1",
     "websocket.js": "^0.1.7",
     "ws": "^2.1.0"
+  },
+  "devDependencies": {
+    "babel-eslint": "^7.2.1",
+    "eslint": "^3.19.0",
+    "eslint-plugin-react": "^6.10.3"
   }
 }
diff --git a/scalingo.json b/scalingo.json
index d60f1529c..4afaa6b4e 100644
--- a/scalingo.json
+++ b/scalingo.json
@@ -71,6 +71,18 @@
       "description": "Address to send emails from",
       "required": false
     },
+    "SMTP_AUTH_METHOD": {
+      "description": "Authentication method to use with SMTP server. Default is 'plain'.",
+      "required": false
+    },
+    "SMTP_OPENSSL_VERIFY_MODE": {
+      "description": "SMTP server certificate verification mode. Defaults is 'peer'.",
+      "required": false
+    },
+    "SMTP_ENABLE_STARTTLS_AUTO": {
+      "description": "Enable STARTTLS if SMTP server supports it? Default is true.",
+      "required": false
+    },
     "BUILDPACK_URL": {
       "description": "Internal scalingo configuration",
       "required": true,
diff --git a/spec/controllers/about_controller_spec.rb b/spec/controllers/about_controller_spec.rb
index 4282649e1..f49de9622 100644
--- a/spec/controllers/about_controller_spec.rb
+++ b/spec/controllers/about_controller_spec.rb
@@ -3,9 +3,16 @@ require 'rails_helper'
 RSpec.describe AboutController, type: :controller do
   render_views
 
-  describe 'GET #index' do
+  describe 'GET #show' do
     it 'returns http success' do
-      get :index
+      get :show
+      expect(response).to have_http_status(:success)
+    end
+  end
+
+  describe 'GET #more' do
+    it 'returns http success' do
+      get :more
       expect(response).to have_http_status(:success)
     end
   end
diff --git a/spec/controllers/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb
new file mode 100644
index 000000000..622ea87c1
--- /dev/null
+++ b/spec/controllers/admin/reports_controller_spec.rb
@@ -0,0 +1,14 @@
+require 'rails_helper'
+
+RSpec.describe Admin::ReportsController, type: :controller do
+  describe 'GET #index' do
+    before do
+      sign_in Fabricate(:user, admin: true), scope: :user
+    end
+
+    it 'returns http success' do
+      get :index
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb
new file mode 100644
index 000000000..c126b645b
--- /dev/null
+++ b/spec/controllers/admin/settings_controller_spec.rb
@@ -0,0 +1,14 @@
+require 'rails_helper'
+
+RSpec.describe Admin::SettingsController, type: :controller do
+  describe 'GET #index' do
+    before do
+      sign_in Fabricate(:user, admin: true), scope: :user
+    end
+
+    it 'returns http success' do
+      get :index
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index 5d36b0159..ed49779b4 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -24,6 +24,45 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     end
   end
 
+  describe 'PATCH #update_credentials' do
+    describe 'with valid data' do
+      before do
+        avatar = File.read(Rails.root.join('app', 'assets', 'images', 'logo.png'))
+        header = File.read(Rails.root.join('app', 'assets', 'images', 'mastodon-getting-started.png'))
+
+        patch :update_credentials, params: {
+          display_name: "Alice Isn't Dead",
+          note: "Hi!\n\nToot toot!",
+          avatar: "data:image/png;base64,#{Base64.encode64(avatar)}",
+          header: "data:image/png;base64,#{Base64.encode64(header)}",
+        }
+      end
+
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+
+      it 'updates account info' do
+        user.account.reload
+
+        expect(user.account.display_name).to eq("Alice Isn't Dead")
+        expect(user.account.note).to eq("Hi!\n\nToot toot!")
+        expect(user.account.avatar).to exist
+        expect(user.account.header).to exist
+      end
+    end
+
+    describe 'with invalid data' do
+      before do
+        patch :update_credentials, params: { note: 'This is too long. ' * 10 }
+      end
+
+      it 'returns http unprocessable entity' do
+        expect(response).to have_http_status(:unprocessable_entity)
+      end
+    end
+  end
+
   describe 'GET #statuses' do
     it 'returns http success' do
       get :statuses, params: { id: user.account.id }
diff --git a/spec/controllers/api/v1/notifications_controller_spec.rb b/spec/controllers/api/v1/notifications_controller_spec.rb
index e5f7eec73..c390d4f01 100644
--- a/spec/controllers/api/v1/notifications_controller_spec.rb
+++ b/spec/controllers/api/v1/notifications_controller_spec.rb
@@ -5,15 +5,71 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
   let(:token) { double acceptable?: true, resource_owner_id: user.id }
+  let(:other) { Fabricate(:user, account: Fabricate(:account, username: 'bob')) }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
   describe 'GET #index' do
-    it 'returns http success' do
-      get :index
-      expect(response).to have_http_status(:success)
+    before do
+      status     = PostStatusService.new.call(user.account, 'Test')
+      @reblog    = ReblogService.new.call(other.account, status)
+      @mention   = PostStatusService.new.call(other.account, 'Hello @alice')
+      @favourite = FavouriteService.new.call(other.account, status)
+      @follow    = FollowService.new.call(other.account, 'alice')
+    end
+
+    describe 'with no options' do
+      before do
+        get :index
+      end
+
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+
+      it 'includes reblog' do
+        expect(assigns(:notifications).map(&:activity_id)).to include(@reblog.id)
+      end
+
+      it 'includes mention' do
+        expect(assigns(:notifications).map(&:activity_id)).to include(@mention.mentions.first.id)
+      end
+
+      it 'includes favourite' do
+        expect(assigns(:notifications).map(&:activity_id)).to include(@favourite.id)
+      end
+
+      it 'includes follow' do
+        expect(assigns(:notifications).map(&:activity_id)).to include(@follow.id)
+      end
+    end
+
+    describe 'with excluded mentions' do
+      before do
+        get :index, params: { exclude_types: ['mention'] }
+      end
+
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+
+      it 'includes reblog' do
+        expect(assigns(:notifications).map(&:activity_id)).to include(@reblog.id)
+      end
+
+      it 'excludes mention' do
+        expect(assigns(:notifications).map(&:activity_id)).to_not include(@mention.mentions.first.id)
+      end
+
+      it 'includes favourite' do
+        expect(assigns(:notifications).map(&:activity_id)).to include(@favourite.id)
+      end
+
+      it 'includes follow' do
+        expect(assigns(:notifications).map(&:activity_id)).to include(@follow.id)
+      end
     end
   end
 end
diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb
index 27ad6cbde..6b26e6693 100644
--- a/spec/controllers/auth/registrations_controller_spec.rb
+++ b/spec/controllers/auth/registrations_controller_spec.rb
@@ -5,6 +5,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
 
   describe 'GET #new' do
     before do
+      Setting.open_registrations = true
       request.env["devise.mapping"] = Devise.mappings[:user]
     end
 
@@ -16,6 +17,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
 
   describe 'POST #create' do
     before do
+      Setting.open_registrations = true
       request.env["devise.mapping"] = Devise.mappings[:user]
       post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } }
     end
diff --git a/spec/controllers/xrd_controller_spec.rb b/spec/controllers/xrd_controller_spec.rb
index e687cf9e0..b56c68f5c 100644
--- a/spec/controllers/xrd_controller_spec.rb
+++ b/spec/controllers/xrd_controller_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe XrdController, type: :controller do
     let(:alice) { Fabricate(:account, username: 'alice') }
 
     it 'returns http success when account can be found' do
-      get :webfinger, params: { resource: "acct:#{alice.username}@#{Rails.configuration.x.local_domain}" }
+      get :webfinger, params: { resource: alice.to_webfinger_s }
       expect(response).to have_http_status(:success)
     end
 
diff --git a/spec/fixtures/files/avatar.gif b/spec/fixtures/files/avatar.gif
new file mode 100644
index 000000000..d929801e5
--- /dev/null
+++ b/spec/fixtures/files/avatar.gif
Binary files differdiff --git a/spec/helpers/about_helper_spec.rb b/spec/helpers/about_helper_spec.rb
deleted file mode 100644
index 6efc9f5bd..000000000
--- a/spec/helpers/about_helper_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe AboutHelper, type: :helper do
-
-end
diff --git a/spec/helpers/accounts_helper_spec.rb b/spec/helpers/accounts_helper_spec.rb
deleted file mode 100644
index 3aea1f909..000000000
--- a/spec/helpers/accounts_helper_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe AccountsHelper, type: :helper do
-
-end
diff --git a/spec/helpers/admin/domain_blocks_helper_spec.rb b/spec/helpers/admin/domain_blocks_helper_spec.rb
deleted file mode 100644
index cc7ead84e..000000000
--- a/spec/helpers/admin/domain_blocks_helper_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe Admin::DomainBlocksHelper, type: :helper do
-
-end
diff --git a/spec/helpers/admin/pubsubhubbub_helper_spec.rb b/spec/helpers/admin/pubsubhubbub_helper_spec.rb
deleted file mode 100644
index 673236a7e..000000000
--- a/spec/helpers/admin/pubsubhubbub_helper_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe Admin::PubsubhubbubHelper, type: :helper do
-
-end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index c2063c995..a2eeb443c 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -1,5 +1,19 @@
 require 'rails_helper'
 
-RSpec.describe ApplicationHelper, type: :helper do
+describe ApplicationHelper do
+  describe 'active_nav_class' do
+    it 'returns active when on the current page' do
+      allow(helper).to receive(:current_page?).and_return(true)
 
+      result = helper.active_nav_class("/test")
+      expect(result).to eq "active"
+    end
+
+    it 'returns empty string when not on current page' do
+      allow(helper).to receive(:current_page?).and_return(false)
+
+      result = helper.active_nav_class("/test")
+      expect(result).to eq ""
+    end
+  end
 end
diff --git a/spec/helpers/authorize_follow_helper_spec.rb b/spec/helpers/authorize_follow_helper_spec.rb
deleted file mode 100644
index ba5b0a70b..000000000
--- a/spec/helpers/authorize_follow_helper_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe AuthorizeFollowHelper, type: :helper do
-
-end
diff --git a/spec/helpers/stream_entries_helper_spec.rb b/spec/helpers/stream_entries_helper_spec.rb
index 6227f9280..221e1e32d 100644
--- a/spec/helpers/stream_entries_helper_spec.rb
+++ b/spec/helpers/stream_entries_helper_spec.rb
@@ -2,7 +2,17 @@ require 'rails_helper'
 
 RSpec.describe StreamEntriesHelper, type: :helper do
   describe '#display_name' do
-    pending
+    it 'uses the display name when it exists' do
+      account = Account.new(display_name: "Display", username: "Username")
+
+      expect(helper.display_name(account)).to eq "Display"
+    end
+
+    it 'uses the username when display name is nil' do
+      account = Account.new(display_name: nil, username: "Username")
+
+      expect(helper.display_name(account)).to eq "Username"
+    end
   end
 
   describe '#avatar_for_status_url' do
diff --git a/spec/helpers/tags_helper_spec.rb b/spec/helpers/tags_helper_spec.rb
deleted file mode 100644
index f661e44ac..000000000
--- a/spec/helpers/tags_helper_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe TagsHelper, type: :helper do
-
-end
diff --git a/spec/helpers/xrd_helper_spec.rb b/spec/helpers/xrd_helper_spec.rb
deleted file mode 100644
index 0bc71b657..000000000
--- a/spec/helpers/xrd_helper_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe XrdHelper, type: :helper do
-
-end
diff --git a/spec/javascript/components/avatar.test.jsx b/spec/javascript/components/avatar.test.jsx
index 852e13a89..7131bbec7 100644
--- a/spec/javascript/components/avatar.test.jsx
+++ b/spec/javascript/components/avatar.test.jsx
@@ -6,16 +6,10 @@ import Avatar from '../../../app/assets/javascripts/components/components/avatar
 describe('<Avatar />', () => {
   const src = '/path/to/image.jpg';
   const size = 100;
-  const wrapper = render(<Avatar src={src} size={size} />);
+  const wrapper = render(<Avatar src={src} animate size={size} />);
 
-  it('renders an img element with the given src', () => {
-    expect(wrapper.find('img')).to.have.attr('src', `${src}`);
-  });
-
-  it('renders an img element of the given size', () => {
-    ['width', 'height'].map((attr) => {
-      expect(wrapper.find('img')).to.have.attr(attr, `${size}`);
-    });
+  it('renders a div element with the given src as background', () => {
+    expect(wrapper.find('div')).to.have.style('background-image', `url(${src})`);
   });
 
   it('renders a div element of the given size', () => {
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 93a45459d..fb367ab7a 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -54,6 +54,30 @@ RSpec.describe Account, type: :model do
     end
   end
 
+  describe 'Local domain user methods' do
+    around do |example|
+      before = Rails.configuration.x.local_domain
+      example.run
+      Rails.configuration.x.local_domain = before
+    end
+
+    describe '#to_webfinger_s' do
+      it 'returns a webfinger string for the account' do
+        Rails.configuration.x.local_domain = 'example.com'
+
+        expect(subject.to_webfinger_s).to eq 'acct:alice@example.com'
+      end
+    end
+
+    describe '#local_username_and_domain' do
+      it 'returns the username and local domain for the account' do
+        Rails.configuration.x.local_domain = 'example.com'
+
+        expect(subject.local_username_and_domain).to eq 'alice@example.com'
+      end
+    end
+  end
+
   describe '#acct' do
     it 'returns username for local users' do
       expect(subject.acct).to eql 'alice'
@@ -170,6 +194,61 @@ RSpec.describe Account, type: :model do
     end
   end
 
+  describe '.search_for' do
+    before do
+      @match = Fabricate(
+        :account,
+        display_name: "Display Name",
+        username: "username",
+        domain: "example.com"
+      )
+      _missing = Fabricate(
+        :account,
+        display_name: "Missing",
+        username: "missing",
+        domain: "missing.com"
+      )
+    end
+
+    it 'finds accounts with matching display_name' do
+      results = Account.search_for("display")
+      expect(results).to eq [@match]
+    end
+
+    it 'finds accounts with matching username' do
+      results = Account.search_for("username")
+      expect(results).to eq [@match]
+    end
+
+    it 'finds accounts with matching domain' do
+      results = Account.search_for("example")
+      expect(results).to eq [@match]
+    end
+
+    it 'ranks multiple matches higher' do
+      account = Fabricate(
+        :account,
+        username: "username",
+        display_name: "username"
+      )
+      results = Account.search_for("username")
+      expect(results).to eq [account, @match]
+    end
+  end
+
+  describe '.advanced_search_for' do
+    it 'ranks followed accounts higher' do
+      account = Fabricate(:account)
+      match = Fabricate(:account, username: "Matching")
+      followed_match = Fabricate(:account, username: "Matcher")
+      Fabricate(:follow, account: account, target_account: followed_match)
+
+      results = Account.advanced_search_for("match", account)
+      expect(results).to eq [followed_match, match]
+      expect(results.first.rank).to be > results.last.rank
+    end
+  end
+
   describe '.find_local' do
     before do
       Fabricate(:account, username: 'Alice')
@@ -342,4 +421,24 @@ RSpec.describe Account, type: :model do
       end
     end
   end
+
+  describe 'static avatars' do
+    describe 'when GIF' do
+      it 'creates a png static style' do
+        subject.avatar = attachment_fixture('avatar.gif')
+        subject.save
+
+        expect(subject.avatar_static_url).to_not eq subject.avatar_original_url
+      end
+    end
+
+    describe 'when non-GIF' do
+      it 'does not create extra static style' do
+        subject.avatar = attachment_fixture('attachment.jpg')
+        subject.save
+
+        expect(subject.avatar_static_url).to eq subject.avatar_original_url
+      end
+    end
+  end
 end
diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb
index 360bbc16d..7a5b8ec89 100644
--- a/spec/models/tag_spec.rb
+++ b/spec/models/tag_spec.rb
@@ -12,4 +12,15 @@ RSpec.describe Tag, type: :model do
       expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit')).to be_nil
     end
   end
+
+  describe '.search_for' do
+    it 'finds tag records with matching names' do
+      tag = Fabricate(:tag, name: "match")
+      _miss_tag = Fabricate(:tag, name: "miss")
+
+      results = Tag.search_for("match")
+
+      expect(results).to eq [tag]
+    end
+  end
 end
diff --git a/spec/presenters/instance_presenter_spec.rb b/spec/presenters/instance_presenter_spec.rb
new file mode 100644
index 000000000..0f318d9c3
--- /dev/null
+++ b/spec/presenters/instance_presenter_spec.rb
@@ -0,0 +1,74 @@
+require 'rails_helper'
+
+describe InstancePresenter do
+  let(:instance_presenter) { InstancePresenter.new }
+
+  it "delegates site_description to Setting" do
+    Setting.site_description = "Site desc"
+
+    expect(instance_presenter.site_description).to eq "Site desc"
+  end
+
+  it "delegates site_extended_description to Setting" do
+    Setting.site_extended_description = "Extended desc"
+
+    expect(instance_presenter.site_extended_description).to eq "Extended desc"
+  end
+
+  it "delegates open_registrations to Setting" do
+    Setting.open_registrations = false
+
+    expect(instance_presenter.open_registrations).to eq false
+  end
+
+  it "delegates closed_registrations_message to Setting" do
+    Setting.closed_registrations_message = "Closed message"
+
+    expect(instance_presenter.closed_registrations_message).to eq "Closed message"
+  end
+
+  it "delegates contact_email to Setting" do
+    Setting.contact_email = "admin@example.com"
+
+    expect(instance_presenter.contact_email).to eq "admin@example.com"
+  end
+
+  describe "contact_account" do
+    it "returns the account for the site contact username" do
+      Setting.site_contact_username = "aaa"
+      account = Fabricate(:account, username: "aaa")
+
+      expect(instance_presenter.contact_account).to eq(account)
+    end
+  end
+
+  describe "user_count" do
+    it "returns the number of site users" do
+      cache = double
+      allow(Rails).to receive(:cache).and_return(cache)
+      allow(cache).to receive(:fetch).with("user_count").and_return(123)
+
+      expect(instance_presenter.user_count).to eq(123)
+    end
+  end
+
+  describe "status_count" do
+    it "returns the number of local statuses" do
+      cache = double
+      allow(Rails).to receive(:cache).and_return(cache)
+      allow(cache).to receive(:fetch).with("local_status_count").and_return(234)
+
+      expect(instance_presenter.status_count).to eq(234)
+    end
+  end
+
+  describe "domain_count" do
+    it "returns the number of known domains" do
+      cache = double
+      allow(Rails).to receive(:cache).and_return(cache)
+      allow(cache).to receive(:fetch).with("distinct_domain_count").and_return(345)
+
+      expect(instance_presenter.domain_count).to eq(345)
+    end
+  end
+end
diff --git a/spec/requests/catch_all_route_request_spec.rb b/spec/requests/catch_all_route_request_spec.rb
new file mode 100644
index 000000000..22ce1cf59
--- /dev/null
+++ b/spec/requests/catch_all_route_request_spec.rb
@@ -0,0 +1,21 @@
+require "rails_helper"
+
+describe "The catch all route" do
+  describe "with a simple value" do
+    it "returns a 404 page as html" do
+      get "/test"
+
+      expect(response.status).to eq 404
+      expect(response.content_type).to eq "text/html"
+    end
+  end
+
+  describe "with an implied format" do
+    it "returns a 404 page as html" do
+      get "/test.test"
+
+      expect(response.status).to eq 404
+      expect(response.content_type).to eq "text/html"
+    end
+  end
+end
diff --git a/streaming/index.js b/streaming/index.js
index 7edf6203f..a1e7eaca7 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -87,21 +87,24 @@ const setRequestId = (req, res, next) => {
 const accountFromToken = (token, req, next) => {
   pgPool.connect((err, client, done) => {
     if (err) {
-      return next(err)
+      next(err)
+      return
     }
 
     client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 LIMIT 1', [token], (err, result) => {
       done()
 
       if (err) {
-        return next(err)
+        next(err)
+        return
       }
 
       if (result.rows.length === 0) {
         err = new Error('Invalid access token')
         err.statusCode = 401
 
-        return next(err)
+        next(err)
+        return
       }
 
       req.accountId = result.rows[0].account_id
@@ -113,7 +116,8 @@ const accountFromToken = (token, req, next) => {
 
 const authenticationMiddleware = (req, res, next) => {
   if (req.method === 'OPTIONS') {
-    return next()
+    next()
+    return
   }
 
   const authorization = req.get('Authorization')
@@ -122,7 +126,8 @@ const authenticationMiddleware = (req, res, next) => {
     const err = new Error('Missing access token')
     err.statusCode = 401
 
-    return next(err)
+    next(err)
+    return
   }
 
   const token = authorization.replace(/^Bearer /, '')
diff --git a/yarn.lock b/yarn.lock
index 6a3a36270..b83924ad9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -140,6 +140,12 @@ acorn-globals@^3.1.0:
   dependencies:
     acorn "^4.0.4"
 
+acorn-jsx@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+  dependencies:
+    acorn "^3.0.4"
+
 acorn@^1.0.3:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-1.2.2.tgz#c8ce27de0acc76d896d2b1fad3df588d9e82f014"
@@ -148,7 +154,7 @@ acorn@^2.7.0:
   version "2.7.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7"
 
-acorn@^3.0.0:
+acorn@^3.0.0, acorn@^3.0.4:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
 
@@ -156,6 +162,10 @@ acorn@^4.0.3, acorn@^4.0.4:
   version "4.0.11"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0"
 
+acorn@^5.0.1:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d"
+
 airbnb-js-shims@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/airbnb-js-shims/-/airbnb-js-shims-1.0.1.tgz#7d5a7d772c8c6fdeb624ea3cef62506091b180b5"
@@ -169,7 +179,7 @@ airbnb-js-shims@^1.0.1:
     string.prototype.padend "^3.0.0"
     string.prototype.padstart "^3.0.0"
 
-ajv-keywords@^1.1.1:
+ajv-keywords@^1.0.0, ajv-keywords@^1.1.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
 
@@ -196,6 +206,10 @@ amdefine@>=0.0.4:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.0.tgz#fd17474700cb5cc9c2b709f0be9d23ce3c198c33"
 
+ansi-escapes@^1.1.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
+
 ansi-html@0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
@@ -284,10 +298,27 @@ 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"
+  dependencies:
+    array-uniq "^1.0.1"
+
+array-uniq@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+
 array-unique@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
 
+array.prototype.find@^2.0.1:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.0.4.tgz#556a5c5362c08648323ddaeb9de9d14bc1864c90"
+  dependencies:
+    define-properties "^1.1.2"
+    es-abstract "^1.7.0"
+
 arrify@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
@@ -424,7 +455,7 @@ babel-code-frame@^6.11.0:
     esutils "^2.0.2"
     js-tokens "^2.0.0"
 
-babel-code-frame@^6.22.0:
+babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
   version "6.22.0"
   resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
   dependencies:
@@ -480,6 +511,15 @@ babel-core@^6.11.4:
     slash "^1.0.0"
     source-map "^0.5.0"
 
+babel-eslint@^7.2.1:
+  version "7.2.1"
+  resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.1.tgz#079422eb73ba811e3ca0865ce87af29327f8c52f"
+  dependencies:
+    babel-code-frame "^6.22.0"
+    babel-traverse "^6.23.1"
+    babel-types "^6.23.0"
+    babylon "^6.16.1"
+
 babel-generator@^6.22.0:
   version "6.22.0"
   resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.22.0.tgz#d642bf4961911a8adc7c692b0c9297f325cda805"
@@ -1302,6 +1342,10 @@ babylon@^6.15.0:
   version "6.15.0"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e"
 
+babylon@^6.16.1:
+  version "6.16.1"
+  resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3"
+
 babylon@~5.8.3:
   version "5.8.38"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-5.8.38.tgz#ec9b120b11bf6ccd4173a18bf217e60b79859ffd"
@@ -1586,6 +1630,16 @@ cached-path-relative@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7"
 
+caller-path@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
+  dependencies:
+    callsites "^0.2.0"
+
+callsites@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+
 camelcase-keys@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
@@ -1639,7 +1693,7 @@ chai@^3.5.0:
     deep-eql "^0.1.3"
     type-detect "^1.0.0"
 
-chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
+chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
   dependencies:
@@ -1695,6 +1749,10 @@ cipher-base@^1.0.0, cipher-base@^1.0.1:
   dependencies:
     inherits "^2.0.1"
 
+circular-json@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d"
+
 clap@^1.0.9:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/clap/-/clap-1.1.1.tgz#a8a93e0bfb7581ac199c4f001a5525a724ce696d"
@@ -1705,6 +1763,16 @@ classnames@^2.1.2, classnames@^2.2.3, classnames@~2.2:
   version "2.2.5"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
 
+cli-cursor@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
+  dependencies:
+    restore-cursor "^1.0.1"
+
+cli-width@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
+
 cliui@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
@@ -1824,7 +1892,7 @@ concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
 
-concat-stream@^1.4.7, concat-stream@~1.5.0, concat-stream@~1.5.1:
+concat-stream@^1.4.7, concat-stream@^1.5.2, concat-stream@~1.5.0, concat-stream@~1.5.1:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266"
   dependencies:
@@ -2085,6 +2153,12 @@ currently-unhandled@^0.4.1:
   dependencies:
     array-find-index "^1.0.1"
 
+d@1:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
+  dependencies:
+    es5-ext "^0.10.9"
+
 d@^0.1.1, d@~0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309"
@@ -2140,6 +2214,18 @@ defined@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
 
+del@^2.0.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
+  dependencies:
+    globby "^5.0.0"
+    is-path-cwd "^1.0.0"
+    is-path-in-cwd "^1.0.0"
+    object-assign "^4.0.1"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+    rimraf "^2.2.8"
+
 delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -2197,6 +2283,13 @@ diffie-hellman@^5.0.0:
     miller-rabin "^4.0.0"
     randombytes "^2.0.0"
 
+doctrine@^1.2.2:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
+  dependencies:
+    esutils "^2.0.2"
+    isarray "^1.0.0"
+
 doctrine@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
@@ -2377,6 +2470,15 @@ es-abstract@^1.3.2, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1:
     is-callable "^1.1.3"
     is-regex "^1.0.3"
 
+es-abstract@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c"
+  dependencies:
+    es-to-primitive "^1.1.1"
+    function-bind "^1.1.0"
+    is-callable "^1.1.3"
+    is-regex "^1.0.3"
+
 es-to-primitive@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d"
@@ -2385,6 +2487,13 @@ es-to-primitive@^1.1.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.1"
 
+es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14:
+  version "0.10.15"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.15.tgz#c330a5934c1ee21284a7c081a86e5fd937c91ea6"
+  dependencies:
+    es6-iterator "2"
+    es6-symbol "~3.1"
+
 es5-ext@^0.10.7, es5-ext@~0.10.11, es5-ext@~0.10.2:
   version "0.10.12"
   resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047"
@@ -2404,10 +2513,39 @@ es6-iterator@2:
     es5-ext "^0.10.7"
     es6-symbol "3"
 
+es6-iterator@^2.0.1, es6-iterator@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512"
+  dependencies:
+    d "1"
+    es5-ext "^0.10.14"
+    es6-symbol "^3.1"
+
+es6-map@^0.1.3:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+    es6-iterator "~2.0.1"
+    es6-set "~0.1.5"
+    es6-symbol "~3.1.1"
+    event-emitter "~0.3.5"
+
 es6-promise@^3.2.1:
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
 
+es6-set@~0.1.5:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+    es6-iterator "~2.0.1"
+    es6-symbol "3.1.1"
+    event-emitter "~0.3.5"
+
 es6-shim@^0.35.1:
   version "0.35.1"
   resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.1.tgz#a23524009005b031ab4a352ac196dfdfd1144ab7"
@@ -2419,6 +2557,22 @@ es6-symbol@3, es6-symbol@^3.0.2, es6-symbol@~3.1:
     d "~0.1.1"
     es5-ext "~0.10.11"
 
+es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+
+es6-weak-map@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
+  dependencies:
+    d "1"
+    es5-ext "^0.10.14"
+    es6-iterator "^2.0.1"
+    es6-symbol "^3.1.1"
+
 escape-html@^1.0.3, escape-html@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -2438,6 +2592,72 @@ escodegen@^1.6.1:
   optionalDependencies:
     source-map "~0.2.0"
 
+escope@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3"
+  dependencies:
+    es6-map "^0.1.3"
+    es6-weak-map "^2.0.1"
+    esrecurse "^4.1.0"
+    estraverse "^4.1.1"
+
+eslint-plugin-react@^6.10.3:
+  version "6.10.3"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-6.10.3.tgz#c5435beb06774e12c7db2f6abaddcbf900cd3f78"
+  dependencies:
+    array.prototype.find "^2.0.1"
+    doctrine "^1.2.2"
+    has "^1.0.1"
+    jsx-ast-utils "^1.3.4"
+    object.assign "^4.0.4"
+
+eslint@^3.19.0:
+  version "3.19.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc"
+  dependencies:
+    babel-code-frame "^6.16.0"
+    chalk "^1.1.3"
+    concat-stream "^1.5.2"
+    debug "^2.1.1"
+    doctrine "^2.0.0"
+    escope "^3.6.0"
+    espree "^3.4.0"
+    esquery "^1.0.0"
+    estraverse "^4.2.0"
+    esutils "^2.0.2"
+    file-entry-cache "^2.0.0"
+    glob "^7.0.3"
+    globals "^9.14.0"
+    ignore "^3.2.0"
+    imurmurhash "^0.1.4"
+    inquirer "^0.12.0"
+    is-my-json-valid "^2.10.0"
+    is-resolvable "^1.0.0"
+    js-yaml "^3.5.1"
+    json-stable-stringify "^1.0.0"
+    levn "^0.3.0"
+    lodash "^4.0.0"
+    mkdirp "^0.5.0"
+    natural-compare "^1.4.0"
+    optionator "^0.8.2"
+    path-is-inside "^1.0.1"
+    pluralize "^1.2.1"
+    progress "^1.1.8"
+    require-uncached "^1.0.2"
+    shelljs "^0.7.5"
+    strip-bom "^3.0.0"
+    strip-json-comments "~2.0.1"
+    table "^3.7.8"
+    text-table "~0.2.0"
+    user-home "^2.0.0"
+
+espree@^3.4.0:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.1.tgz#28a83ab4aaed71ed8fe0f5efe61b76a05c13c4d2"
+  dependencies:
+    acorn "^5.0.1"
+    acorn-jsx "^3.0.0"
+
 esprima@^2.6.0, esprima@^2.7.1:
   version "2.7.3"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
@@ -2446,10 +2666,31 @@ esprima@~3.1.0:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
 
+esquery@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+  dependencies:
+    estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220"
+  dependencies:
+    estraverse "~4.1.0"
+    object-assign "^4.0.1"
+
 estraverse@^1.9.1:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44"
 
+estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+estraverse@~4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2"
+
 esutils@^2.0.0, esutils@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
@@ -2458,6 +2699,13 @@ etag@~1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8"
 
+event-emitter@~0.3.5:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+
 events@^1.0.0, events@^1.1.1, events@~1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
@@ -2478,6 +2726,10 @@ exenv@1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.0.tgz#3835f127abf075bfe082d0aed4484057c78e3c89"
 
+exit-hook@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
+
 expand-brackets@^0.1.4:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
@@ -2559,6 +2811,20 @@ fbjs@^0.8.1, fbjs@^0.8.4:
     promise "^7.1.1"
     ua-parser-js "^0.7.9"
 
+figures@^1.3.5:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
+  dependencies:
+    escape-string-regexp "^1.0.5"
+    object-assign "^4.1.0"
+
+file-entry-cache@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
+  dependencies:
+    flat-cache "^1.2.1"
+    object-assign "^4.0.1"
+
 file-loader@^0.9.0:
   version "0.9.0"
   resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.9.0.tgz#1d2daddd424ce6d1b07cfe3f79731bed3617ab42"
@@ -2604,6 +2870,15 @@ find-up@^1.0.0:
     path-exists "^2.0.0"
     pinkie-promise "^2.0.0"
 
+flat-cache@^1.2.1:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96"
+  dependencies:
+    circular-json "^0.3.1"
+    del "^2.0.2"
+    graceful-fs "^4.1.2"
+    write "^0.2.1"
+
 flatten@1.0.2, flatten@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
@@ -2815,10 +3090,21 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.0, glob@~7.1.1:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-globals@^9.0.0:
+globals@^9.0.0, globals@^9.14.0:
   version "9.14.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034"
 
+globby@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
+  dependencies:
+    array-union "^1.0.1"
+    arrify "^1.0.0"
+    glob "^7.0.3"
+    object-assign "^4.0.1"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+
 globule@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/globule/-/globule-1.1.0.tgz#c49352e4dc183d85893ee825385eb994bb6df45f"
@@ -2986,6 +3272,10 @@ ieee754@^1.1.4:
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
 
+ignore@^3.2.0:
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.7.tgz#4810ca5f1d8eca5595213a34b94f2eb4ed926bbd"
+
 immutable@^3.7.6, immutable@^3.8.1:
   version "3.8.1"
   resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2"
@@ -3037,6 +3327,24 @@ inline-source-map@~0.6.0:
   dependencies:
     source-map "~0.5.3"
 
+inquirer@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
+  dependencies:
+    ansi-escapes "^1.1.0"
+    ansi-regex "^2.0.0"
+    chalk "^1.0.0"
+    cli-cursor "^1.0.1"
+    cli-width "^2.0.0"
+    figures "^1.3.5"
+    lodash "^4.3.0"
+    readline2 "^1.0.1"
+    run-async "^0.1.0"
+    rx-lite "^3.1.2"
+    string-width "^1.0.1"
+    strip-ansi "^3.0.0"
+    through "^2.3.6"
+
 insert-module-globals@^7.0.0:
   version "7.0.1"
   resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.0.1.tgz#c03bf4e01cb086d5b5e5ace8ad0afe7889d638c3"
@@ -3162,13 +3470,17 @@ is-fullwidth-code-point@^1.0.0:
   dependencies:
     number-is-nan "^1.0.0"
 
+is-fullwidth-code-point@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
 is-glob@^2.0.0, is-glob@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
   dependencies:
     is-extglob "^1.0.0"
 
-is-my-json-valid@^2.12.4:
+is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4:
   version "2.15.0"
   resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b"
   dependencies:
@@ -3187,6 +3499,22 @@ is-obj@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
 
+is-path-cwd@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
+
+is-path-in-cwd@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc"
+  dependencies:
+    is-path-inside "^1.0.0"
+
+is-path-inside@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f"
+  dependencies:
+    path-is-inside "^1.0.1"
+
 is-plain-obj@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
@@ -3217,6 +3545,12 @@ is-regexp@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
 
+is-resolvable@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62"
+  dependencies:
+    tryit "^1.0.1"
+
 is-stream@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@@ -3298,7 +3632,7 @@ js-tokens@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
 
-js-yaml@^3.4.3, js-yaml@~3.6.1:
+js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@~3.6.1:
   version "3.6.1"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.6.1.tgz#6e5fe67d8b205ce4d22fad05b7781e8dadcc4b30"
   dependencies:
@@ -3349,7 +3683,7 @@ json-schema@0.2.3:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
 
-json-stable-stringify@^1.0.1:
+json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
   dependencies:
@@ -3397,6 +3731,12 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     verror "1.3.6"
 
+jsx-ast-utils@^1.3.4:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.0.tgz#5afe38868f56bc8cc7aeaef0100ba8c75bd12591"
+  dependencies:
+    object-assign "^4.1.0"
+
 keycode@^2.1.1:
   version "2.1.7"
   resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.7.tgz#7b9255919f6cff562b09a064d222dca70b020f5c"
@@ -3435,7 +3775,7 @@ lcid@^1.0.0:
   dependencies:
     invert-kv "^1.0.0"
 
-levn@~0.3.0:
+levn@^0.3.0, levn@~0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
   dependencies:
@@ -3634,7 +3974,7 @@ lodash.tail@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
 
-lodash@4.x.x, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.1:
+lodash@4.x.x, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.1, lodash@^4.3.0:
   version "4.17.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
 
@@ -3865,10 +4205,18 @@ ms@0.7.2:
   version "0.7.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"
 
+mute-stream@0.0.5:
+  version "0.0.5"
+  resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
+
 nan@^2.3.0, nan@^2.3.2, nan@~2.5.0:
   version "2.5.1"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2"
 
+natural-compare@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
 negotiator@0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
@@ -4156,6 +4504,10 @@ once@~1.3.0, once@~1.3.3:
   dependencies:
     wrappy "1"
 
+onetime@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
+
 optimist@~0.6.0:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
@@ -4163,7 +4515,7 @@ optimist@~0.6.0:
     minimist "~0.0.1"
     wordwrap "~0.0.2"
 
-optionator@^0.8.1:
+optionator@^0.8.1, optionator@^0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
   dependencies:
@@ -4284,6 +4636,10 @@ path-is-absolute@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
 
+path-is-inside@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+
 path-platform@~0.11.15:
   version "0.11.15"
   resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2"
@@ -4373,6 +4729,10 @@ pkg-dir@^1.0.0:
   dependencies:
     find-up "^1.0.0"
 
+pluralize@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
+
 podda@^1.2.1:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/podda/-/podda-1.2.2.tgz#15b0edbd334ade145813343f5ecf9c10a71cf500"
@@ -4718,6 +5078,10 @@ process@^0.11.0, process@~0.11.0:
   version "0.11.9"
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1"
 
+progress@^1.1.8:
+  version "1.1.8"
+  resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
+
 promise@^7.1.1:
   version "7.1.1"
   resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf"
@@ -5140,6 +5504,14 @@ readdirp@^2.0.0:
     readable-stream "^2.0.2"
     set-immediate-shim "^1.0.1"
 
+readline2@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35"
+  dependencies:
+    code-point-at "^1.0.0"
+    is-fullwidth-code-point "^1.0.0"
+    mute-stream "0.0.5"
+
 recast@^0.11.5:
   version "0.11.22"
   resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.22.tgz#dedeb18fb001a2bbc6ac34475fda53dfe3d47dfa"
@@ -5341,6 +5713,13 @@ require-main-filename@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
 
+require-uncached@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
+  dependencies:
+    caller-path "^0.1.0"
+    resolve-from "^1.0.0"
+
 requires-port@1.0.x:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
@@ -5349,17 +5728,28 @@ reselect@^2.5.4:
   version "2.5.4"
   resolved "https://registry.yarnpkg.com/reselect/-/reselect-2.5.4.tgz#b7d23fdf00b83fa7ad0279546f8dbbbd765c7047"
 
+resolve-from@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
+
 resolve@1.1.7, resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
 
+restore-cursor@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
+  dependencies:
+    exit-hook "^1.0.0"
+    onetime "^1.0.0"
+
 right-align@^0.1.1:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
   dependencies:
     align-text "^0.1.1"
 
-rimraf@2, rimraf@~2.5.0, rimraf@~2.5.1:
+rimraf@2, rimraf@^2.2.8, rimraf@~2.5.0, rimraf@~2.5.1:
   version "2.5.4"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04"
   dependencies:
@@ -5373,6 +5763,16 @@ ripemd160@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e"
 
+run-async@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
+  dependencies:
+    once "^1.3.0"
+
+rx-lite@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
+
 samsam@1.1.2, samsam@~1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567"
@@ -5523,6 +5923,14 @@ shelljs@^0.7.4:
     interpret "^1.0.0"
     rechoir "^0.6.2"
 
+shelljs@^0.7.5:
+  version "0.7.7"
+  resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1"
+  dependencies:
+    glob "^7.0.0"
+    interpret "^1.0.0"
+    rechoir "^0.6.2"
+
 signal-exit@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.1.tgz#5a4c884992b63a7acd9badb7894c3ee9cfccad81"
@@ -5548,6 +5956,10 @@ slash@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
 
+slice-ansi@0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
+
 slide@^1.1.5:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
@@ -5692,6 +6104,13 @@ string-width@^1.0.1, string-width@^1.0.2:
     is-fullwidth-code-point "^1.0.0"
     strip-ansi "^3.0.0"
 
+string-width@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e"
+  dependencies:
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^3.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"
@@ -5735,6 +6154,10 @@ strip-bom@^2.0.0:
   dependencies:
     is-utf8 "^0.2.0"
 
+strip-bom@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
 strip-indent@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
@@ -5745,6 +6168,10 @@ strip-json-comments@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
 
+strip-json-comments@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
 style-loader@0.13.1:
   version "0.13.1"
   resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.13.1.tgz#468280efbc0473023cd3a6cd56e33b5a1d7fc3a9"
@@ -5809,6 +6236,17 @@ syntax-error@^1.1.1:
   dependencies:
     acorn "^2.7.0"
 
+table@^3.7.8:
+  version "3.8.3"
+  resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
+  dependencies:
+    ajv "^4.7.0"
+    ajv-keywords "^1.0.0"
+    chalk "^1.1.1"
+    lodash "^4.0.0"
+    slice-ansi "0.0.4"
+    string-width "^2.0.0"
+
 tapable@^0.1.8, tapable@~0.1.8:
   version "0.1.10"
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4"
@@ -5856,6 +6294,10 @@ tar@^2.0.0, tar@~2.2.0, tar@~2.2.1:
     fstream "^1.0.2"
     inherits "2"
 
+text-table@~0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+
 through2@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.1.tgz#384e75314d49f32de12eebb8136b8eb6b5d59da9"
@@ -5863,7 +6305,7 @@ through2@^2.0.0:
     readable-stream "~2.0.0"
     xtend "~4.0.0"
 
-through@2, "through@>=2.2.7 <3":
+through@2, "through@>=2.2.7 <3", through@^2.3.6:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
 
@@ -5909,6 +6351,10 @@ trim-right@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
 
+tryit@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
+
 tty-browserify@0.0.0, tty-browserify@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@@ -6022,6 +6468,12 @@ user-home@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190"
 
+user-home@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
+  dependencies:
+    os-homedir "^1.0.0"
+
 utf-8-validate@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-3.0.1.tgz#5d2b8656b4ddcfded47217b647a98941b63cf213"
@@ -6280,6 +6732,12 @@ write-file-atomic@^1.1.2:
     imurmurhash "^0.1.4"
     slide "^1.1.5"
 
+write@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
+  dependencies:
+    mkdirp "^0.5.1"
+
 ws@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/ws/-/ws-2.1.0.tgz#b24eaed9609f8632dd51e3f7698619a90fddcc92"