about summary refs log tree commit diff
diff options
context:
space:
mode:
authorOndřej Hruška <ondra@ondrovo.com>2017-08-07 22:43:52 +0200
committerOndřej Hruška <ondra@ondrovo.com>2017-08-07 22:43:52 +0200
commit7a1ca8b0df9d82a1e6ac4943a2d5562f93eab69f (patch)
tree36fbf6f3d2019cc345d33fe555569ec750a66957
parentb8791ae79be7a9b7c0e541928b2741e40bbdc8a5 (diff)
parenta3e53bd442752f210db2025f2dfc45e7599354c2 (diff)
Merge remote-tracking branch 'upstream/master'
-rw-r--r--.travis.yml1
-rw-r--r--Dockerfile22
-rw-r--r--Gemfile.lock120
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/auth/passwords_controller.rb15
-rw-r--r--app/controllers/authorize_follows_controller.rb2
-rw-r--r--app/controllers/remote_follow_controller.rb2
-rw-r--r--app/javascript/mastodon/actions/statuses.js17
-rw-r--r--app/javascript/mastodon/components/column.js2
-rw-r--r--app/javascript/mastodon/components/display_name.js7
-rw-r--r--app/javascript/mastodon/components/status.js26
-rw-r--r--app/javascript/mastodon/components/status_content.js6
-rw-r--r--app/javascript/mastodon/components/status_list.js2
-rw-r--r--app/javascript/mastodon/containers/status_container.js6
-rw-r--r--app/javascript/mastodon/emoji.js48
-rw-r--r--app/javascript/mastodon/emojione_light.js2
-rw-r--r--app/javascript/mastodon/features/account/components/header.js13
-rw-r--r--app/javascript/mastodon/features/compose/components/reply_indicator.js3
-rw-r--r--app/javascript/mastodon/features/follow_requests/components/account_authorize.js3
-rw-r--r--app/javascript/mastodon/features/notifications/components/notification.js7
-rw-r--r--app/javascript/mastodon/features/report/components/status_check_box.js3
-rw-r--r--app/javascript/mastodon/features/ui/components/column.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js14
-rw-r--r--app/javascript/mastodon/features/ui/index.js4
-rw-r--r--app/javascript/mastodon/locales/fa.json4
-rw-r--r--app/javascript/mastodon/locales/fr.json16
-rw-r--r--app/javascript/mastodon/locales/pl.json6
-rw-r--r--app/javascript/mastodon/reducers/accounts.js6
-rw-r--r--app/javascript/mastodon/reducers/statuses.js26
-rw-r--r--app/javascript/mastodon/scroll.js11
-rw-r--r--app/javascript/styles/accounts.scss4
-rw-r--r--app/javascript/styles/basics.scss2
-rw-r--r--app/javascript/styles/compact_header.scss8
-rw-r--r--app/javascript/styles/components.scss4
-rw-r--r--app/javascript/styles/containers.scss56
-rw-r--r--app/javascript/styles/forms.scss2
-rw-r--r--app/lib/formatter.rb2
-rw-r--r--app/models/concerns/account_avatar.rb2
-rw-r--r--app/models/concerns/account_header.rb2
-rw-r--r--app/views/authorize_follows/show.html.haml9
-rw-r--r--app/views/layouts/modal.html.haml16
-rw-r--r--app/views/tags/show.html.haml1
-rw-r--r--config/application.rb2
-rw-r--r--config/i18n-tasks.yml4
-rw-r--r--config/initializers/doorkeeper.rb5
-rw-r--r--config/locales/ar.yml1
-rw-r--r--config/locales/bg.yml1
-rw-r--r--config/locales/ca.yml1
-rw-r--r--config/locales/de.yml1
-rw-r--r--config/locales/devise.pl.yml10
-rw-r--r--config/locales/doorkeeper.pl.yml2
-rw-r--r--config/locales/en.yml5
-rw-r--r--config/locales/eo.yml1
-rw-r--r--config/locales/es.yml1
-rw-r--r--config/locales/fa.yml17
-rw-r--r--config/locales/fi.yml1
-rw-r--r--config/locales/fr.yml7
-rw-r--r--config/locales/he.yml1
-rw-r--r--config/locales/hr.yml1
-rw-r--r--config/locales/id.yml1
-rw-r--r--config/locales/io.yml1
-rw-r--r--config/locales/it.yml1
-rw-r--r--config/locales/ja.yml4
-rw-r--r--config/locales/ko.yml1
-rw-r--r--config/locales/nl.yml13
-rw-r--r--config/locales/no.yml1
-rw-r--r--config/locales/oc.yml6
-rw-r--r--config/locales/pl.yml20
-rw-r--r--config/locales/pt-BR.yml1
-rw-r--r--config/locales/ru.yml7
-rw-r--r--config/locales/simple_form.fa.yml2
-rw-r--r--config/locales/th.yml1
-rw-r--r--config/locales/tr.yml1
-rw-r--r--config/locales/uk.yml1
-rw-r--r--config/locales/zh-CN.yml1
-rw-r--r--config/locales/zh-HK.yml1
-rw-r--r--config/locales/zh-TW.yml1
-rw-r--r--config/webpack/loaders/babel.js3
-rw-r--r--lib/mastodon/version.rb2
-rw-r--r--spec/controllers/auth/passwords_controller_spec.rb25
-rw-r--r--spec/controllers/auth/sessions_controller_spec.rb4
-rw-r--r--spec/javascript/components/display_name.test.js12
82 files changed, 419 insertions, 260 deletions
diff --git a/.travis.yml b/.travis.yml
index 4d4dc0893..d5b51fcb0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,6 +6,7 @@ cache:
   - node_modules
   - public/assets
   - public/packs-test
+  - tmp/cache/babel-loader
 dist: trusty
 sudo: required
 
diff --git a/Dockerfile b/Dockerfile
index ef139dcec..398628a48 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,6 +7,9 @@ ENV UID=991 GID=991 \
     RAILS_SERVE_STATIC_FILES=true \
     RAILS_ENV=production NODE_ENV=production
 
+ARG LIBICONV_VERSION=1.15
+ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
+
 EXPOSE 3000 4000
 
 WORKDIR /mastodon
@@ -18,8 +21,7 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
     build-base \
     icu-dev \
     libidn-dev \
-    libxml2-dev \
-    libxslt-dev \
+    libtool \
     postgresql-dev \
     protobuf-dev \
     python \
@@ -32,8 +34,6 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
     imagemagick@edge \
     libidn \
     libpq \
-    libxml2 \
-    libxslt \
     nodejs-npm@edge \
     nodejs@edge \
     protobuf \
@@ -41,11 +41,23 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
     tini \
     yarn@edge \
  && update-ca-certificates \
+ && wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
+ && echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
+ && mkdir -p /tmp/src \
+ && tar -xzf libiconv.tar.gz -C /tmp/src \
+ && rm libiconv.tar.gz \
+ && cd /tmp/src/libiconv-$LIBICONV_VERSION \
+ && ./configure --prefix=/usr/local \
+ && make -j$(getconf _NPROCESSORS_ONLN)\
+ && make install \
+ && libtool --finish /usr/local/lib \
+ && cd /mastodon \
  && rm -rf /tmp/* /var/cache/apk/*
 
 COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
 
-RUN bundle install --deployment --without test development \
+RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \
+ && bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
  && yarn --ignore-optional --pure-lockfile
 
 COPY . /mastodon
diff --git a/Gemfile.lock b/Gemfile.lock
index e5fd3506a..7a4dbab85 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,25 +1,25 @@
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (5.1.2)
-      actionpack (= 5.1.2)
+    actioncable (5.1.3)
+      actionpack (= 5.1.3)
       nio4r (~> 2.0)
       websocket-driver (~> 0.6.1)
-    actionmailer (5.1.2)
-      actionpack (= 5.1.2)
-      actionview (= 5.1.2)
-      activejob (= 5.1.2)
+    actionmailer (5.1.3)
+      actionpack (= 5.1.3)
+      actionview (= 5.1.3)
+      activejob (= 5.1.3)
       mail (~> 2.5, >= 2.5.4)
       rails-dom-testing (~> 2.0)
-    actionpack (5.1.2)
-      actionview (= 5.1.2)
-      activesupport (= 5.1.2)
+    actionpack (5.1.3)
+      actionview (= 5.1.3)
+      activesupport (= 5.1.3)
       rack (~> 2.0)
       rack-test (~> 0.6.3)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.0.2)
-    actionview (5.1.2)
-      activesupport (= 5.1.2)
+    actionview (5.1.3)
+      activesupport (= 5.1.3)
       builder (~> 3.1)
       erubi (~> 1.4)
       rails-dom-testing (~> 2.0)
@@ -30,16 +30,16 @@ GEM
       case_transform (>= 0.2)
       jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
     active_record_query_trace (1.5.4)
-    activejob (5.1.2)
-      activesupport (= 5.1.2)
+    activejob (5.1.3)
+      activesupport (= 5.1.3)
       globalid (>= 0.3.6)
-    activemodel (5.1.2)
-      activesupport (= 5.1.2)
-    activerecord (5.1.2)
-      activemodel (= 5.1.2)
-      activesupport (= 5.1.2)
+    activemodel (5.1.3)
+      activesupport (= 5.1.3)
+    activerecord (5.1.3)
+      activemodel (= 5.1.3)
+      activesupport (= 5.1.3)
       arel (~> 8.0)
-    activesupport (5.1.2)
+    activesupport (5.1.3)
       concurrent-ruby (~> 1.0, >= 1.0.2)
       i18n (~> 0.7)
       minitest (~> 5.1)
@@ -57,14 +57,14 @@ GEM
       encryptor (~> 3.0.0)
     av (0.9.0)
       cocaine (~> 0.5.3)
-    aws-sdk (2.10.6)
-      aws-sdk-resources (= 2.10.6)
-    aws-sdk-core (2.10.6)
+    aws-sdk (2.10.21)
+      aws-sdk-resources (= 2.10.21)
+    aws-sdk-core (2.10.21)
       aws-sigv4 (~> 1.0)
       jmespath (~> 1.0)
-    aws-sdk-resources (2.10.6)
-      aws-sdk-core (= 2.10.6)
-    aws-sigv4 (1.0.0)
+    aws-sdk-resources (2.10.21)
+      aws-sdk-core (= 2.10.21)
+    aws-sigv4 (1.0.1)
     bcrypt (3.1.11)
     better_errors (2.1.1)
       coderay (>= 1.0.0)
@@ -72,7 +72,7 @@ GEM
       rack (>= 0.9.0)
     binding_of_caller (0.7.2)
       debug_inspector (>= 0.0.1)
-    bootsnap (1.1.1)
+    bootsnap (1.1.2)
       msgpack (~> 1.0)
     brakeman (3.6.2)
     browser (2.4.0)
@@ -155,7 +155,7 @@ GEM
     et-orbi (1.0.5)
       tzinfo
     execjs (2.7.0)
-    fabrication (2.16.1)
+    fabrication (2.16.2)
     faker (1.7.3)
       i18n (~> 0.5)
     fast_blank (1.0.0)
@@ -165,7 +165,7 @@ GEM
       ruby-progressbar (~> 1.4)
     globalid (0.4.0)
       activesupport (>= 4.2.0)
-    goldfinger (2.0.0)
+    goldfinger (2.0.1)
       addressable (~> 2.5)
       http (~> 2.2)
       nokogiri (~> 1.8)
@@ -179,7 +179,7 @@ GEM
       activesupport (>= 4.0.1)
       hamlit (>= 1.2.0)
       railties (>= 4.0.1)
-    hashdiff (0.3.4)
+    hashdiff (0.3.5)
     highline (1.7.8)
     hiredis (0.6.1)
     hkdf (0.3.0)
@@ -194,11 +194,11 @@ GEM
     http-form_data (1.0.3)
     http_accept_language (2.1.1)
     http_parser.rb (0.6.0)
-    httplog (0.99.4)
+    httplog (0.99.7)
       colorize
       rack
-    i18n (0.8.4)
-    i18n-tasks (0.9.15)
+    i18n (0.8.6)
+    i18n-tasks (0.9.16)
       activesupport (>= 4.0.2)
       ast (>= 2.1.0)
       easy_translate (>= 0.5.0)
@@ -211,7 +211,7 @@ GEM
     idn-ruby (0.1.0)
     jmespath (1.3.1)
     json (2.1.0)
-    jsonapi-renderer (0.1.2)
+    jsonapi-renderer (0.1.3)
     jwt (1.5.6)
     kaminari (1.0.1)
       activesupport (>= 4.1.0)
@@ -253,7 +253,7 @@ GEM
     mime-types-data (3.2016.0521)
     mimemagic (0.3.2)
     mini_portile2 (2.2.0)
-    minitest (5.10.2)
+    minitest (5.10.3)
     msgpack (1.1.0)
     multi_json (1.12.1)
     net-scp (1.2.1)
@@ -264,7 +264,7 @@ GEM
       mini_portile2 (~> 2.2.0)
     nokogumbo (1.4.13)
       nokogiri
-    oj (3.2.0)
+    oj (3.3.4)
     openssl (2.0.4)
     orm_adapter (0.5.0)
     ostatus2 (2.0.1)
@@ -283,14 +283,14 @@ GEM
       av (~> 0.9.0)
       paperclip (>= 2.5.2)
     parallel (1.11.2)
-    parallel_tests (2.14.1)
+    parallel_tests (2.14.2)
       parallel
     parser (2.4.0.0)
       ast (~> 2.2)
     pg (0.21.0)
     pghero (1.7.0)
       activerecord
-    pkg-config (1.2.3)
+    pkg-config (1.2.4)
     powerpack (0.1.1)
     pry (0.10.4)
       coderay (~> 1.1.0)
@@ -313,17 +313,17 @@ GEM
     rack-test (0.6.3)
       rack (>= 1.0)
     rack-timeout (0.4.2)
-    rails (5.1.2)
-      actioncable (= 5.1.2)
-      actionmailer (= 5.1.2)
-      actionpack (= 5.1.2)
-      actionview (= 5.1.2)
-      activejob (= 5.1.2)
-      activemodel (= 5.1.2)
-      activerecord (= 5.1.2)
-      activesupport (= 5.1.2)
-      bundler (>= 1.3.0, < 2.0)
-      railties (= 5.1.2)
+    rails (5.1.3)
+      actioncable (= 5.1.3)
+      actionmailer (= 5.1.3)
+      actionpack (= 5.1.3)
+      actionview (= 5.1.3)
+      activejob (= 5.1.3)
+      activemodel (= 5.1.3)
+      activerecord (= 5.1.3)
+      activesupport (= 5.1.3)
+      bundler (>= 1.3.0)
+      railties (= 5.1.3)
       sprockets-rails (>= 2.0.0)
     rails-controller-testing (1.0.2)
       actionpack (~> 5.x, >= 5.0.1)
@@ -337,11 +337,11 @@ GEM
     rails-i18n (5.0.4)
       i18n (~> 0.7)
       railties (~> 5.0)
-    rails-settings-cached (0.6.5)
+    rails-settings-cached (0.6.6)
       rails (>= 4.2.0)
-    railties (5.1.2)
-      actionpack (= 5.1.2)
-      activesupport (= 5.1.2)
+    railties (5.1.3)
+      actionpack (= 5.1.3)
+      activesupport (= 5.1.3)
       method_source
       rake (>= 0.8.7)
       thor (>= 0.18.1, < 2.0)
@@ -353,7 +353,7 @@ GEM
       actionpack (>= 4.0, < 6)
       redis-rack (>= 1, < 3)
       redis-store (>= 1.1.0, < 1.4.0)
-    redis-activesupport (5.0.2)
+    redis-activesupport (5.0.3)
       activesupport (>= 3, < 6)
       redis-store (~> 1.3.0)
     redis-namespace (1.5.3)
@@ -413,7 +413,7 @@ GEM
     scss_lint (0.54.0)
       rake (>= 0.9, < 13)
       sass (~> 3.4.20)
-    sidekiq (5.0.3)
+    sidekiq (5.0.4)
       concurrent-ruby (~> 1.0)
       connection_pool (~> 2.2, >= 2.2.0)
       rack-protection (>= 1.5.0)
@@ -421,12 +421,12 @@ GEM
     sidekiq-bulk (0.1.1)
       activesupport
       sidekiq
-    sidekiq-scheduler (2.1.7)
+    sidekiq-scheduler (2.1.8)
       redis (~> 3)
       rufus-scheduler (~> 3.2)
       sidekiq (>= 3)
       tilt (>= 1.4.0)
-    sidekiq-unique-jobs (5.0.8)
+    sidekiq-unique-jobs (5.0.9)
       sidekiq (>= 4.0, <= 6.0)
       thor (~> 0)
     simple-navigation (4.0.5)
@@ -450,15 +450,15 @@ GEM
     sshkit (1.13.1)
       net-scp (>= 1.1.2)
       net-ssh (>= 2.8.0)
-    statsd-instrument (2.1.2)
+    statsd-instrument (2.1.4)
     temple (0.8.0)
     terminal-table (1.8.0)
       unicode-display_width (~> 1.1, >= 1.1.1)
     thor (0.19.4)
     thread (0.2.2)
     thread_safe (0.3.6)
-    tilt (2.0.7)
-    twitter-text (1.14.6)
+    tilt (2.0.8)
+    twitter-text (1.14.7)
       unf (~> 0.1.0)
     tzinfo (1.2.3)
       thread_safe (~> 0.1)
@@ -590,4 +590,4 @@ RUBY VERSION
    ruby 2.4.1p111
 
 BUNDLED WITH
-   1.15.2
+   1.15.3
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b3c2db02b..0b40fb05b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -43,6 +43,10 @@ class ApplicationController < ActionController::Base
     forbidden if current_user.account.suspended?
   end
 
+  def after_sign_out_path_for(_resource_or_scope)
+    new_user_session_path
+  end
+
   protected
 
   def forbidden
diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb
index 54ee1c39c..171b997dc 100644
--- a/app/controllers/auth/passwords_controller.rb
+++ b/app/controllers/auth/passwords_controller.rb
@@ -1,5 +1,20 @@
 # frozen_string_literal: true
 
 class Auth::PasswordsController < Devise::PasswordsController
+  before_action :check_validity_of_reset_password_token, only: :edit
+
   layout 'auth'
+
+  private
+
+  def check_validity_of_reset_password_token
+    unless reset_password_token_is_valid?
+      flash[:error] = I18n.t('auth.invalid_reset_password_token')
+      redirect_to new_password_path(resource_name)
+    end
+  end
+
+  def reset_password_token_is_valid?
+    resource_class.with_reset_password_token(params[:reset_password_token]).present?
+  end
 end
diff --git a/app/controllers/authorize_follows_controller.rb b/app/controllers/authorize_follows_controller.rb
index dccd1c209..78b564183 100644
--- a/app/controllers/authorize_follows_controller.rb
+++ b/app/controllers/authorize_follows_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class AuthorizeFollowsController < ApplicationController
-  layout 'public'
+  layout 'modal'
 
   before_action :authenticate_user!
 
diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb
index 2988231b1..48b026aa5 100644
--- a/app/controllers/remote_follow_controller.rb
+++ b/app/controllers/remote_follow_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class RemoteFollowController < ApplicationController
-  layout 'public'
+  layout 'modal'
 
   before_action :set_account
   before_action :gone, if: :suspended_account?
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 2204e0b14..0b5e72c17 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -23,6 +23,9 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
 export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
 export const STATUS_UNMUTE_FAIL    = 'STATUS_UNMUTE_FAIL';
 
+export const STATUS_SET_HEIGHT = 'STATUS_SET_HEIGHT';
+export const STATUSES_CLEAR_HEIGHT = 'STATUSES_CLEAR_HEIGHT';
+
 export function fetchStatusRequest(id, skipLoading) {
   return {
     type: STATUS_FETCH_REQUEST,
@@ -215,3 +218,17 @@ export function unmuteStatusFail(id, error) {
     error,
   };
 };
+
+export function setStatusHeight (id, height) {
+  return {
+    type: STATUS_SET_HEIGHT,
+    id,
+    height,
+  };
+};
+
+export function clearStatusesHeight () {
+  return {
+    type: STATUSES_CLEAR_HEIGHT,
+  };
+};
diff --git a/app/javascript/mastodon/components/column.js b/app/javascript/mastodon/components/column.js
index 29c8f4389..103fcd495 100644
--- a/app/javascript/mastodon/components/column.js
+++ b/app/javascript/mastodon/components/column.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import detectPassiveEvents from 'detect-passive-events';
-import scrollTop from '../scroll';
+import { scrollTop } from '../scroll';
 
 export default class Column extends React.PureComponent {
 
diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js
index dc3665a2b..2cf84f8f4 100644
--- a/app/javascript/mastodon/components/display_name.js
+++ b/app/javascript/mastodon/components/display_name.js
@@ -1,7 +1,5 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import escapeTextContentForBrowser from 'escape-html';
-import emojify from '../emoji';
 
 export default class DisplayName extends React.PureComponent {
 
@@ -10,12 +8,11 @@ export default class DisplayName extends React.PureComponent {
   };
 
   render () {
-    const displayName     = this.props.account.get('display_name').length === 0 ? this.props.account.get('username') : this.props.account.get('display_name');
-    const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
+    const displayNameHtml = { __html: this.props.account.get('display_name_html') };
 
     return (
       <span className='display-name'>
-        <strong className='display-name__html' dangerouslySetInnerHTML={displayNameHTML} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
+        <strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
       </span>
     );
   }
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 9bc3523c8..7468957d3 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -11,8 +11,6 @@ import DisplayName from './display_name';
 import StatusContent from './status_content';
 import StatusActionBar from './status_action_bar';
 import { FormattedMessage } from 'react-intl';
-import emojify from '../emoji';
-import escapeTextContentForBrowser from 'escape-html';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
 import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components';
@@ -39,6 +37,7 @@ export default class Status extends ImmutablePureComponent {
     onOpenMedia: PropTypes.func,
     onOpenVideo: PropTypes.func,
     onBlock: PropTypes.func,
+    onHeightChange: PropTypes.func,
     me: PropTypes.number,
     boostModal: PropTypes.bool,
     autoPlayGif: PropTypes.bool,
@@ -50,7 +49,6 @@ export default class Status extends ImmutablePureComponent {
 
   state = {
     isExpanded: false,
-    isIntersecting: true, // assume intersecting until told otherwise
     isHidden: false, // set to true in requestIdleCallback to trigger un-render
   }
 
@@ -111,6 +109,10 @@ export default class Status extends ImmutablePureComponent {
     if (this.node && this.node.children.length !== 0) {
       // save the height of the fully-rendered element
       this.height = getRectFromEntry(entry).height;
+
+      if (this.props.onHeightChange) {
+        this.props.onHeightChange(this.props.status, this.height);
+      }
     }
 
     this.setState((prevState) => {
@@ -182,9 +184,13 @@ export default class Status extends ImmutablePureComponent {
       return null;
     }
 
-    if (!isIntersecting && isHidden) {
+    const hasIntersectionObserverWrapper = !!this.props.intersectionObserverWrapper;
+    const isHiddenForSure = isIntersecting === false && isHidden;
+    const visibilityUnknownButHeightIsCached = isIntersecting === undefined && status.has('height');
+
+    if (hasIntersectionObserverWrapper && (isHiddenForSure || visibilityUnknownButHeightIsCached)) {
       return (
-        <article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}>
+        <article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height || status.get('height')}px`, opacity: 0, overflow: 'hidden' }}>
           {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
           {status.get('content')}
         </article>
@@ -192,19 +198,13 @@ export default class Status extends ImmutablePureComponent {
     }
 
     if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
-      let displayName = status.getIn(['account', 'display_name']);
-
-      if (displayName.length === 0) {
-        displayName = status.getIn(['account', 'username']);
-      }
-
-      const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
+      const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
 
       return (
         <article className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0'>
           <div className='status__prepend'>
             <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
-            <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
+            <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={display_name_html} /></a> }} />
           </div>
 
           <Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} />
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 5f02e3261..d1381f176 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -3,9 +3,7 @@
 
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import escapeTextContentForBrowser from 'escape-html';
 import PropTypes from 'prop-types';
-import emojify from '../emoji';
 import { isRtl } from '../rtl';
 import { FormattedMessage } from 'react-intl';
 import Permalink from './permalink';
@@ -122,8 +120,8 @@ export default class StatusContent extends React.PureComponent {
 
     const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
 
-    const content = { __html: emojify(status.get('content')) };
-    const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
+    const content = { __html: status.get('contentHtml') };
+    const spoilerContent = { __html: status.get('spoilerHtml') };
     const directionStyle = { direction: 'ltr' };
     const classNames = classnames('status__content', {
       'status__content--with-action': this.props.onClick && this.context.router,
diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js
index 639c8b4e7..271cf33b7 100644
--- a/app/javascript/mastodon/components/status_list.js
+++ b/app/javascript/mastodon/components/status_list.js
@@ -105,7 +105,7 @@ export default class StatusList extends ImmutablePureComponent {
   }
 
   handleKeyDown = (e) => {
-    if (['PageDown', 'PageUp', 'End', 'Home'].includes(e.key)) {
+    if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
       const article = (() => {
         switch (e.key) {
         case 'PageDown':
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index 9b7f984e0..d71584267 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -19,7 +19,7 @@ import {
   blockAccount,
   muteAccount,
 } from '../actions/accounts';
-import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
+import { muteStatus, unmuteStatus, deleteStatus, setStatusHeight } from '../actions/statuses';
 import { initReport } from '../actions/reports';
 import { openModal } from '../actions/modal';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@@ -127,6 +127,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }
   },
 
+  onHeightChange (status, height) {
+    dispatch(setStatusHeight(status.get('id'), height));
+  },
+
 });
 
 export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js
index 5695c86dd..a41dfdd1d 100644
--- a/app/javascript/mastodon/emoji.js
+++ b/app/javascript/mastodon/emoji.js
@@ -3,34 +3,28 @@ import Trie from 'substring-trie';
 
 const trie = new Trie(Object.keys(unicodeMapping));
 
-const excluded = ['™', '©', '®'];
-
-function emojify(str) {
-  // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
-  // and replacing valid unicode strings
-  // that _aren't_ within tags with an <img> version.
-  // The goal is to be the same as an emojione.regUnicode replacement, but faster.
-  let i = -1;
-  let insideTag = false;
-  let match;
-  while (++i < str.length) {
-    const char = str.charAt(i);
-    if (insideTag && char === '>') {
-      insideTag = false;
-    } else if (char === '<') {
-      insideTag = true;
-    } else if (!insideTag && (match = trie.search(str.substring(i)))) {
-      const unicodeStr = match;
-      if (unicodeStr in unicodeMapping && excluded.indexOf(unicodeStr) === -1) {
-        const [filename, shortCode] = unicodeMapping[unicodeStr];
-        const alt      = unicodeStr;
-        const replacement =  `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;
-        str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length);
-        i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string
-      }
+const emojify = str => {
+  let rtn = '';
+  for (;;) {
+    let match, i = 0;
+    while (i < str.length && str[i] !== '<' && !(match = trie.search(str.slice(i)))) {
+      i += str.codePointAt(i) < 65536 ? 1 : 2;
+    }
+    if (i === str.length)
+      break;
+    else if (str[i] === '<') {
+      let tagend = str.indexOf('>', i + 1) + 1;
+      if (!tagend)
+        break;
+      rtn += str.slice(0, tagend);
+      str = str.slice(tagend);
+    } else {
+      const [filename, shortCode] = unicodeMapping[match];
+      rtn += str.slice(0, i) + `<img draggable="false" class="emojione" alt="${match}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;
+      str = str.slice(i + match.length);
     }
   }
-  return str;
-}
+  return rtn + str;
+};
 
 export default emojify;
diff --git a/app/javascript/mastodon/emojione_light.js b/app/javascript/mastodon/emojione_light.js
index 985e9dbcb..0d07d012f 100644
--- a/app/javascript/mastodon/emojione_light.js
+++ b/app/javascript/mastodon/emojione_light.js
@@ -4,8 +4,10 @@
 const emojione = require('emojione');
 
 const mappedUnicode = emojione.mapUnicodeToShort();
+const excluded = ['®', '©', '™'];
 
 module.exports.unicodeMapping = Object.keys(emojione.jsEscapeMap)
+  .filter(c => !excluded.includes(c))
   .map(unicodeStr => [unicodeStr, mappedUnicode[emojione.jsEscapeMap[unicodeStr]]])
   .map(([unicodeStr, shortCode]) => ({ [unicodeStr]: [emojione.emojioneList[shortCode].fname, shortCode.slice(1, shortCode.length - 1)] }))
   .reduce((x, y) => Object.assign(x, y), { });
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 9d7bc82c0..320e669a2 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -4,8 +4,6 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import emojify from '../../../emoji';
-import escapeTextContentForBrowser from 'escape-html';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import IconButton from '../../../components/icon_button';
 import Motion from 'react-motion/lib/Motion';
@@ -95,15 +93,10 @@ export default class Header extends ImmutablePureComponent {
       return null;
     }
 
-    let displayName = account.get('display_name');
     let info        = '';
     let actionBtn   = '';
     let lockedIcon  = '';
 
-    if (displayName.length === 0) {
-      displayName = account.get('username');
-    }
-
     if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
       info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>;
     }
@@ -128,15 +121,15 @@ export default class Header extends ImmutablePureComponent {
       lockedIcon = <i className='fa fa-lock' />;
     }
 
-    const content         = { __html: emojify(account.get('note')) };
-    const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
+    const content         = { __html: account.get('note_emojified') };
+    const displayNameHtml = { __html: account.get('display_name_html') };
 
     return (
       <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
         <div>
           <Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
 
-          <span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
+          <span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} />
           <span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
           <div className='account__header__content' dangerouslySetInnerHTML={content} />
 
diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.js
index 35a9b4b1b..7672440b4 100644
--- a/app/javascript/mastodon/features/compose/components/reply_indicator.js
+++ b/app/javascript/mastodon/features/compose/components/reply_indicator.js
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
 import Avatar from '../../../components/avatar';
 import IconButton from '../../../components/icon_button';
 import DisplayName from '../../../components/display_name';
-import emojify from '../../../emoji';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
@@ -43,7 +42,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
       return null;
     }
 
-    const content  = { __html: emojify(status.get('content')) };
+    const content  = { __html: status.get('contentHtml') };
 
     return (
       <div className='reply-indicator'>
diff --git a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js
index 66fa5c235..4fc5638d9 100644
--- a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js
+++ b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js
@@ -4,7 +4,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import Permalink from '../../../components/permalink';
 import Avatar from '../../../components/avatar';
 import DisplayName from '../../../components/display_name';
-import emojify from '../../../emoji';
 import IconButton from '../../../components/icon_button';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -26,7 +25,7 @@ export default class AccountAuthorize extends ImmutablePureComponent {
 
   render () {
     const { intl, account, onAuthorize, onReject } = this.props;
-    const content = { __html: emojify(account.get('note')) };
+    const content = { __html: account.get('note_emojified') };
 
     return (
       <div className='account-authorize__wrapper'>
diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js
index 0771849c2..7d521e4b6 100644
--- a/app/javascript/mastodon/features/notifications/components/notification.js
+++ b/app/javascript/mastodon/features/notifications/components/notification.js
@@ -7,8 +7,6 @@ import StatusContainer from '../../../containers/status_container';
 import AccountContainer from '../../../containers/account_container';
 import { FormattedMessage } from 'react-intl';
 import Permalink from '../../../components/permalink';
-import emojify from '../../../emoji';
-import escapeTextContentForBrowser from 'escape-html';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 export default class Notification extends ImmutablePureComponent {
@@ -70,9 +68,8 @@ export default class Notification extends ImmutablePureComponent {
   render () {
     const { notification } = this.props;
     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' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
+    const displayNameHtml  = { __html: account.get('display_name_html') };
+    const link             = <Permalink className='notification__display-name' 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/javascript/mastodon/features/report/components/status_check_box.js b/app/javascript/mastodon/features/report/components/status_check_box.js
index 6a1a84c28..cc9232201 100644
--- a/app/javascript/mastodon/features/report/components/status_check_box.js
+++ b/app/javascript/mastodon/features/report/components/status_check_box.js
@@ -1,7 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import emojify from '../../../emoji';
 import Toggle from 'react-toggle';
 
 export default class StatusCheckBox extends React.PureComponent {
@@ -15,7 +14,7 @@ export default class StatusCheckBox extends React.PureComponent {
 
   render () {
     const { status, checked, onToggle, disabled } = this.props;
-    const content = { __html: emojify(status.get('content')) };
+    const content = { __html: status.get('contentHtml') };
 
     if (status.get('reblog')) {
       return null;
diff --git a/app/javascript/mastodon/features/ui/components/column.js b/app/javascript/mastodon/features/ui/components/column.js
index aea102aac..9031c16fc 100644
--- a/app/javascript/mastodon/features/ui/components/column.js
+++ b/app/javascript/mastodon/features/ui/components/column.js
@@ -2,7 +2,7 @@ import React from 'react';
 import ColumnHeader from './column_header';
 import PropTypes from 'prop-types';
 import { debounce } from 'lodash';
-import scrollTop from '../../../scroll';
+import { scrollTop } from '../../../scroll';
 import { isMobile } from '../../../is_mobile';
 
 export default class Column extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 63bd1b021..47d5a2e20 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -12,6 +12,8 @@ import ColumnLoading from './column_loading';
 import BundleColumnError from './bundle_column_error';
 import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
 
+import { scrollRight } from '../../../scroll';
+
 const componentMap = {
   'COMPOSE': Compose,
   'HOME': HomeTimeline,
@@ -49,9 +51,13 @@ export default class ColumnsArea extends ImmutablePureComponent {
     this.setState({ shouldAnimate: true });
   }
 
-  componentDidUpdate() {
+  componentDidUpdate(prevProps) {
     this.lastIndex = getIndex(this.context.router.history.location.pathname);
     this.setState({ shouldAnimate: true });
+
+    if (this.props.children !== prevProps.children && !this.props.singleColumn) {
+      scrollRight(this.node);
+    }
   }
 
   handleSwipe = (index) => {
@@ -74,6 +80,10 @@ export default class ColumnsArea extends ImmutablePureComponent {
     }
   }
 
+  setRef = (node) => {
+    this.node = node;
+  }
+
   renderView = (link, index) => {
     const columnIndex = getIndex(this.context.router.history.location.pathname);
     const title = this.props.intl.formatMessage({ id: link.props['data-preview-title-id'] });
@@ -114,7 +124,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
     }
 
     return (
-      <div className='columns-area'>
+      <div className='columns-area' ref={this.setRef}>
         {columns.map(column => {
           const params = column.get('params', null) === null ? null : column.get('params').toJS();
 
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index f7a6eb319..6d53f474d 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -12,6 +12,7 @@ import { debounce } from 'lodash';
 import { uploadCompose } from '../../actions/compose';
 import { refreshHomeTimeline } from '../../actions/timelines';
 import { refreshNotifications } from '../../actions/notifications';
+import { clearStatusesHeight } from '../../actions/statuses';
 import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
 import UploadArea from './components/upload_area';
 import ColumnsAreaContainer from './containers/columns_area_container';
@@ -72,6 +73,9 @@ export default class UI extends React.PureComponent {
   };
 
   handleResize = debounce(() => {
+    // The cached heights are no longer accurate, invalidate
+    this.props.dispatch(clearStatusesHeight());
+
     this.setState({ width: window.innerWidth });
   }, 500, {
     trailing: true,
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index d2682ef12..5ada62f93 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -1,7 +1,7 @@
 {
   "account.block": "مسدودسازی @{name}",
   "account.block_domain": "پنهان‌سازی همه چیز از سرور {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.disclaimer_full": "اطلاعات زیر ممکن است نمایهٔ این کاربر را به تمامی نشان ندهد.",
   "account.edit_profile": "ویرایش نمایه",
   "account.follow": "پی بگیرید",
   "account.followers": "پیگیران",
@@ -13,7 +13,7 @@
   "account.posts": "نوشته‌ها",
   "account.report": "گزارش @{name}",
   "account.requested": "در انتظار پذیرش",
-  "account.share": "Share @{name}'s profile",
+  "account.share": "هم‌رسانی نمایهٔ @{name}",
   "account.unblock": "رفع انسداد @{name}",
   "account.unblock_domain": "رفع پنهان‌سازی از {domain}",
   "account.unfollow": "پایان پیگیری",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index ad9060d25..f3f0d0463 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -13,7 +13,7 @@
   "account.posts": "Statuts",
   "account.report": "Signaler",
   "account.requested": "Invitation envoyée",
-  "account.share": "Share @{name}'s profile",
+  "account.share": "Partager le profil de @{name}",
   "account.unblock": "Débloquer",
   "account.unblock_domain": "Ne plus masquer {domain}",
   "account.unfollow": "Ne plus suivre",
@@ -35,11 +35,11 @@
   "column.notifications": "Notifications",
   "column.public": "Fil public global",
   "column_back_button.label": "Retour",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
+  "column_header.hide_settings": "Masquer les paramètres",
+  "column_header.moveLeft_settings": "Déplacer la colonne vers la gauche",
+  "column_header.moveRight_settings": "Déplacer la colonne vers la droite",
   "column_header.pin": "Épingler",
-  "column_header.show_settings": "Show settings",
+  "column_header.show_settings": "Afficher les paramètres",
   "column_header.unpin": "Retirer",
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Paramètres",
@@ -94,8 +94,8 @@
   "home.column_settings.show_replies": "Afficher les réponses",
   "home.settings": "Paramètres de la colonne",
   "lightbox.close": "Fermer",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
+  "lightbox.next": "Suivant",
+  "lightbox.previous": "Précédent",
   "loading_indicator.label": "Chargement…",
   "media_gallery.toggle_visible": "Modifier la visibilité",
   "missing_indicator.label": "Non trouvé",
@@ -175,7 +175,7 @@
   "status.report": "Signaler @{name}",
   "status.sensitive_toggle": "Cliquer pour afficher",
   "status.sensitive_warning": "Contenu sensible",
-  "status.share": "Share",
+  "status.share": "Partager",
   "status.show_less": "Replier",
   "status.show_more": "Déplier",
   "status.unmute_conversation": "Ne plus masquer la conversation",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index c42721f64..542230f11 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -73,7 +73,7 @@
   "emoji_button.search": "Szukaj...",
   "emoji_button.symbols": "Symbole",
   "emoji_button.travel": "Podróże i miejsca",
-  "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby odbić piłeczkę!",
+  "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
   "empty_column.hashtag": "Nie ma postów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
   "empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
   "empty_column.home.inactivity": "Strumień jest pusty. Jeżeli nie było Cię tu ostatnio, zostanie on wypełniony wkrótce.",
@@ -159,7 +159,7 @@
   "report.target": "Zgłaszanie {target}",
   "search.placeholder": "Szukaj",
   "search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
-  "standalone.public_title": "Spojrzenie wgłąb…",
+  "standalone.public_title": "Spojrzenie w głąb…",
   "status.cannot_reblog": "Ten post nie może zostać podbity",
   "status.delete": "Usuń",
   "status.favourite": "Ulubione",
@@ -178,7 +178,7 @@
   "status.share": "Udostępnij",
   "status.show_less": "Pokaż mniej",
   "status.show_more": "Pokaż więcej",
-  "status.unmute_conversation": "Cofnij wyciezenie konwersacji",
+  "status.unmute_conversation": "Cofnij wyciszenie konwersacji",
   "tabs_bar.compose": "Napisz",
   "tabs_bar.federated_timeline": "Globalne",
   "tabs_bar.home": "Strona główna",
diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js
index 4d7c3adc9..6442d13be 100644
--- a/app/javascript/mastodon/reducers/accounts.js
+++ b/app/javascript/mastodon/reducers/accounts.js
@@ -44,7 +44,9 @@ import {
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
 import { STORE_HYDRATE } from '../actions/store';
+import emojify from '../emoji';
 import { Map as ImmutableMap, fromJS } from 'immutable';
+import escapeTextContentForBrowser from 'escape-html';
 
 const normalizeAccount = (state, account) => {
   account = { ...account };
@@ -53,6 +55,10 @@ const normalizeAccount = (state, account) => {
   delete account.following_count;
   delete account.statuses_count;
 
+  const displayName = account.display_name.length === 0 ? account.username : account.display_name;
+  account.display_name_html = emojify(escapeTextContentForBrowser(displayName));
+  account.note_emojified = emojify(account.note);
+
   return state.set(account.id, fromJS(account));
 };
 
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index b1b1d0988..3e40b0b42 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -13,6 +13,8 @@ import {
   CONTEXT_FETCH_SUCCESS,
   STATUS_MUTE_SUCCESS,
   STATUS_UNMUTE_SUCCESS,
+  STATUS_SET_HEIGHT,
+  STATUSES_CLEAR_HEIGHT,
 } from '../actions/statuses';
 import {
   TIMELINE_REFRESH_SUCCESS,
@@ -33,7 +35,11 @@ import {
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
 import { SEARCH_FETCH_SUCCESS } from '../actions/search';
+import emojify from '../emoji';
 import { Map as ImmutableMap, fromJS } from 'immutable';
+import escapeTextContentForBrowser from 'escape-html';
+
+const domParser = new DOMParser();
 
 const normalizeStatus = (state, status) => {
   if (!status) {
@@ -49,7 +55,9 @@ const normalizeStatus = (state, status) => {
   }
 
   const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
-  normalStatus.search_index = new DOMParser().parseFromString(searchContent, 'text/html').documentElement.textContent;
+  normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
+  normalStatus.contentHtml = emojify(normalStatus.content);
+  normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''));
 
   return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus)));
 };
@@ -82,6 +90,18 @@ const filterStatuses = (state, relationship) => {
   return state;
 };
 
+const setHeight = (state, id, height) => {
+  return state.update(id, ImmutableMap(), map => map.set('height', height));
+};
+
+const clearHeights = (state) => {
+  state.forEach(status => {
+    state = state.deleteIn([status.get('id'), 'height']);
+  });
+
+  return state;
+};
+
 const initialState = ImmutableMap();
 
 export default function statuses(state = initialState, action) {
@@ -120,6 +140,10 @@ export default function statuses(state = initialState, action) {
     return deleteStatus(state, action.id, action.references);
   case ACCOUNT_BLOCK_SUCCESS:
     return filterStatuses(state, action.relationship);
+  case STATUS_SET_HEIGHT:
+    return setHeight(state, action.id, action.height);
+  case STATUSES_CLEAR_HEIGHT:
+    return clearHeights(state);
   default:
     return state;
   }
diff --git a/app/javascript/mastodon/scroll.js b/app/javascript/mastodon/scroll.js
index c089d37db..44f95b17f 100644
--- a/app/javascript/mastodon/scroll.js
+++ b/app/javascript/mastodon/scroll.js
@@ -1,9 +1,9 @@
 const easingOutQuint = (x, t, b, c, d) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
 
-const scrollTop = (node) => {
+const scroll = (node, key, target) => {
   const startTime = Date.now();
-  const offset    = node.scrollTop;
-  const targetY   = -offset;
+  const offset    = node[key];
+  const gap       = target - offset;
   const duration  = 1000;
   let interrupt   = false;
 
@@ -15,7 +15,7 @@ const scrollTop = (node) => {
       return;
     }
 
-    node.scrollTop = easingOutQuint(0, elapsed, offset, targetY, duration);
+    node[key] = easingOutQuint(0, elapsed, offset, gap, duration);
     requestAnimationFrame(step);
   };
 
@@ -26,4 +26,5 @@ const scrollTop = (node) => {
   };
 };
 
-export default scrollTop;
+export const scrollRight = (node) => scroll(node, 'scrollLeft', node.scrollWidth);
+export const scrollTop = (node) => scroll(node, 'scrollTop', 0);
diff --git a/app/javascript/styles/accounts.scss b/app/javascript/styles/accounts.scss
index 3d5c1a692..5a9105109 100644
--- a/app/javascript/styles/accounts.scss
+++ b/app/javascript/styles/accounts.scss
@@ -7,7 +7,7 @@
   box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
   overflow: hidden;
 
-  @media screen and (max-width: 700px) {
+  @media screen and (max-width: 740px) {
     border-radius: 0;
     box-shadow: none;
   }
@@ -298,7 +298,7 @@
   display: flex;
   flex-wrap: wrap;
 
-  @media screen and (max-width: 700px) {
+  @media screen and (max-width: 740px) {
     border-radius: 0;
     box-shadow: none;
   }
diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss
index 182ea36a4..4e51b555c 100644
--- a/app/javascript/styles/basics.scss
+++ b/app/javascript/styles/basics.scss
@@ -47,7 +47,7 @@ body {
     padding: 0;
   }
 
-  @media screen and (max-width: 360px) {
+  @media screen and (max-width: 400px) {
     padding-bottom: 0;
   }
 }
diff --git a/app/javascript/styles/compact_header.scss b/app/javascript/styles/compact_header.scss
index 27a67135f..cf12fcfec 100644
--- a/app/javascript/styles/compact_header.scss
+++ b/app/javascript/styles/compact_header.scss
@@ -3,9 +3,15 @@
     font-size: 24px;
     line-height: 28px;
     color: $ui-primary-color;
-    overflow: hidden;
     font-weight: 500;
     margin-bottom: 20px;
+    padding: 0 10px;
+    overflow-wrap: break-word;
+
+    @media screen and (max-width: 740px) {
+      text-align: center;
+      padding: 20px 10px 0;
+    }
 
     a {
       color: inherit;
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index 41735c7a4..b5efd560f 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -1835,7 +1835,6 @@
   overflow-y: scroll;
   overflow-x: hidden;
   flex: 1 1 auto;
-  backface-visibility: hidden;
   -webkit-overflow-scrolling: touch;
   @supports(display: grid) { // hack to fix Chrome <57
     contain: strict;
@@ -1853,8 +1852,9 @@
   flex: 0 0 auto;
   font-size: 16px;
   border: 0;
-  text-align: start;
+  text-align: unset;
   padding: 15px;
+  margin: 0;
   z-index: 3;
 
   &:hover {
diff --git a/app/javascript/styles/containers.scss b/app/javascript/styles/containers.scss
index 7dcf2c006..536f4e5a1 100644
--- a/app/javascript/styles/containers.scss
+++ b/app/javascript/styles/containers.scss
@@ -3,7 +3,7 @@
   margin: 0 auto;
   margin-top: 40px;
 
-  @media screen and (max-width: 700px) {
+  @media screen and (max-width: 740px) {
     width: 100%;
     margin: 0;
   }
@@ -13,8 +13,9 @@
   margin: 100px auto;
   margin-bottom: 50px;
 
-  @media screen and (max-width: 360px) {
+  @media screen and (max-width: 400px) {
     margin: 30px auto;
+    margin-bottom: 20px;
   }
 
   h1 {
@@ -42,3 +43,54 @@
     }
   }
 }
+
+.account-header {
+  width: 400px;
+  margin: 0 auto;
+  display: flex;
+  font-size: 13px;
+  line-height: 18px;
+  box-sizing: border-box;
+  padding: 20px 0;
+  padding-bottom: 0;
+  margin-bottom: -30px;
+  margin-top: 40px;
+
+  @media screen and (max-width: 400px) {
+    width: 100%;
+    margin: 0;
+    margin-bottom: 10px;
+    padding: 20px;
+    padding-bottom: 0;
+  }
+
+  .avatar {
+    width: 40px;
+    height: 40px;
+    margin-right: 8px;
+
+    img {
+      width: 100%;
+      height: 100%;
+      display: block;
+      margin: 0;
+      border-radius: 4px;
+    }
+  }
+
+  .name {
+    flex: 1 1 auto;
+    color: $ui-secondary-color;
+
+    .username {
+      display: block;
+      font-weight: 500;
+    }
+  }
+
+  .logout-link {
+    display: block;
+    font-size: 32px;
+    line-height: 40px;
+  }
+}
diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss
index cffb6f197..62094e98e 100644
--- a/app/javascript/styles/forms.scss
+++ b/app/javascript/styles/forms.scss
@@ -317,7 +317,7 @@ code {
 }
 
 .flash-message {
-  background: $ui-base-color;
+  background: lighten($ui-base-color, 8%);
   color: $ui-primary-color;
   border-radius: 4px;
   padding: 15px 10px;
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 7b89305ac..cacc0364f 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -104,7 +104,7 @@ class Formatter
     html_attrs     = { target: '_blank', rel: 'nofollow noopener' }
 
     Twitter::Autolink.send(:link_to_text, entity, link_html(entity[:url]), normalized_url, html_attrs)
-  rescue Addressable::URI::InvalidURIError
+  rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
     encode(entity[:url])
   end
 
diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb
index a6527a85b..b0ec689a7 100644
--- a/app/models/concerns/account_avatar.rb
+++ b/app/models/concerns/account_avatar.rb
@@ -8,7 +8,7 @@ module AccountAvatar
   class_methods do
     def avatar_styles(file)
       styles = { original: '120x120#' }
-      styles[:static] = { format: 'png' } if file.content_type == 'image/gif'
+      styles[:static] = { animated: false } if file.content_type == 'image/gif'
       styles
     end
 
diff --git a/app/models/concerns/account_header.rb b/app/models/concerns/account_header.rb
index 4ba9212a2..542e25abe 100644
--- a/app/models/concerns/account_header.rb
+++ b/app/models/concerns/account_header.rb
@@ -8,7 +8,7 @@ module AccountHeader
   class_methods do
     def header_styles(file)
       styles = { original: '700x335#' }
-      styles[:static] = { format: 'png' } if file.content_type == 'image/gif'
+      styles[:static] = { animated: false } if file.content_type == 'image/gif'
       styles
     end
 
diff --git a/app/views/authorize_follows/show.html.haml b/app/views/authorize_follows/show.html.haml
index 3b60df058..f7a8f72d2 100644
--- a/app/views/authorize_follows/show.html.haml
+++ b/app/views/authorize_follows/show.html.haml
@@ -3,10 +3,9 @@
 
 .form-container
   .follow-prompt
-    %h2= t('authorize_follow.prompt_html', self: current_account.username)
-
     = render 'card', account: @account
 
-  = form_tag authorize_follow_path, method: :post, class: 'simple_form' do
-    = hidden_field_tag :acct, @account.acct
-    = button_tag t('authorize_follow.follow'), type: :submit
+  - unless current_account.following?(@account)
+    = form_tag authorize_follow_path, method: :post, class: 'simple_form' do
+      = hidden_field_tag :acct, @account.acct
+      = button_tag t('authorize_follow.follow'), type: :submit
diff --git a/app/views/layouts/modal.html.haml b/app/views/layouts/modal.html.haml
new file mode 100644
index 000000000..a819e098d
--- /dev/null
+++ b/app/views/layouts/modal.html.haml
@@ -0,0 +1,16 @@
+- content_for :header_tags do
+  = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
+
+- content_for :content do
+  - if user_signed_in?
+    .account-header
+      .avatar= image_tag current_account.avatar.url(:original)
+      .name
+        = t 'users.signed_in_as'
+        %span.username @#{current_account.local_username_and_domain}
+      = link_to destroy_user_session_path, method: :delete, class: 'logout-link icon-button' do
+        = fa_icon 'sign-out'
+
+  .container= yield
+
+= render template: 'layouts/application'
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index 15bf714c2..8cd2f1825 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -4,6 +4,7 @@
 .compact-header
   %h1<
     = link_to site_title, root_path
+    %br
     %small ##{@tag.name}
 
 - if @statuses.empty?
diff --git a/config/application.rb b/config/application.rb
index ed5fdb7f7..eb89f0a10 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -86,7 +86,7 @@ module Mastodon
     config.middleware.use Rack::Deflater
 
     config.to_prepare do
-      Doorkeeper::AuthorizationsController.layout 'public'
+      Doorkeeper::AuthorizationsController.layout 'modal'
       Doorkeeper::AuthorizedApplicationsController.layout 'admin'
       Doorkeeper::Application.send :include, ApplicationExtension
     end
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index f2bb220a6..849e8116a 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -42,7 +42,9 @@ ignore_missing:
   - 'simple_form.{error_notification,required}.:'
   - 'errors.messages.*'
   - 'activerecord.errors.models.doorkeeper/*'
-
+  - 'sessions.{browsers,platforms}.*'
+  - 'terms.body_html'
+  - 'application_mailer.salutation'
 ignore_unused:
   - 'activemodel.errors.*'
   - 'activerecord.attributes.*'
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index b618bf344..056a3651a 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -34,6 +34,11 @@ Doorkeeper.configure do
   # https://github.com/doorkeeper-gem/doorkeeper#custom-access-token-generator
   # access_token_generator "::Doorkeeper::JWT"
 
+  # The controller Doorkeeper::ApplicationController inherits from.
+  # Defaults to ActionController::Base.
+  # https://github.com/doorkeeper-gem/doorkeeper#custom-base-controller
+  base_controller 'ApplicationController'
+
   # Reuse access token for the same resource owner within an application (disabled by default)
   # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383
   reuse_access_token
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index ec051591a..575c5114c 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -43,7 +43,6 @@ ar:
   authorize_follow:
     error: Unfortunately, there was an error looking up the remote account
     follow: إتبع
-    prompt_html: 'You (<strong>%{self}</strong>) have requested to follow:'
     title: إتباع %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index 65ff5c025..e7c3e1ef6 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -43,7 +43,6 @@ bg:
   authorize_follow:
     error: Възникна грешка в откриването на потребителя
     follow: Последвай
-    prompt_html: "(<strong>%{self}</strong>), молбата ти беше изпратена до:"
     title: Последвай %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 725b120ec..a9f9e4c93 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -185,7 +185,6 @@ ca:
   authorize_follow:
     error: Malauradament, ha ocorregut un error buscant el compte remot
     follow: Seguir
-    prompt_html: 'Tú (<strong>%{self}</strong>) has solicitat seguir:'
     title: Seguir %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 87c5fa67a..1f3675f47 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -166,7 +166,6 @@ de:
   authorize_follow:
     error: Das Profil konnte nicht geladen werden
     follow: Folgen
-    prompt_html: 'Du (<strong>%{self}</strong>) möchtest dieser Person folgen:'
     title: "%{acct} folgen"
   datetime:
     distance_in_words:
diff --git a/config/locales/devise.pl.yml b/config/locales/devise.pl.yml
index 1c692f7a8..d537efc6e 100644
--- a/config/locales/devise.pl.yml
+++ b/config/locales/devise.pl.yml
@@ -12,9 +12,9 @@ pl:
       last_attempt: Masz jeszcze jedną próbę; Twoje konto zostanie zablokowane jeśli się nie powiedzie.
       locked: Twoje konto zostało zablokowane.
       not_found_in_database: Nieprawidłowy %{authentication_keys} lub hasło.
-      timeout: Twoja sesja wygasła. Zaloguj się ponownie aby kontynuować..
-      unauthenticated: Zapisz się lub zaloguj aby kontynuować.
-      unconfirmed: Zweryfikuj adres e-mail aby kontynuować.
+      timeout: Twoja sesja wygasła. Zaloguj się ponownie, aby kontynuować..
+      unauthenticated: Zapisz się lub zaloguj, aby kontynuować.
+      unconfirmed: Zweryfikuj adres e-mail, aby kontynuować.
     mailer:
       confirmation_instructions:
         subject: 'Mastodon: Instrukcje weryfikacji adresu e-mail'
@@ -38,7 +38,7 @@ pl:
       signed_up: Twoje konto zostało utworzone. Witamy!
       signed_up_but_inactive: Twoje konto zostało utworzone. Nie mogliśmy Cię jednak zalogować, ponieważ konto nie zostało jeszcze aktywowane.
       signed_up_but_locked: Twoje konto zostało utworzone. Nie mogliśmy Cię jednak zalogować, ponieważ konto jest zablokowane.
-      signed_up_but_unconfirmed: Na Twój adres e-mail została wysłana wiadomosć z odnośnikiem potwierdzającym. Kliknij w odnośnik aby aktywować konto. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
+      signed_up_but_unconfirmed: Na Twój adres e-mail została wysłana wiadomosć z odnośnikiem potwierdzającym. Kliknij w odnośnik, aby aktywować konto. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
       update_needs_confirmation: Konto zostało zaktualizowane, musimy jednak zweryfikować Twój nowy adres e-mail. Została na niego wysłana wiadomość z odnośnikiem potwierdzającym. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
       updated: Konto zostało zaktualizowane.
     sessions:
@@ -48,7 +48,7 @@ pl:
     unlocks:
       send_instructions: W ciągu kilku minut otrzymasz wiadomość e-mail z instrukcjami odblokowania konta. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
       send_paranoid_instructions: Jeśli Twoje konto istnieje, instrukcje odblokowania go otrzymasz w wiadomości e-mail w ciągu kilku minut. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
-      unlocked: Twoje konto zostało odblokowane. Zaloguj się aby kontynuować.
+      unlocked: Twoje konto zostało odblokowane. Zaloguj się, aby kontynuować.
   errors:
     messages:
       already_confirmed: był już potwierdzony, spróbuj się zalogować
diff --git a/config/locales/doorkeeper.pl.yml b/config/locales/doorkeeper.pl.yml
index 8103c4561..72b967e35 100644
--- a/config/locales/doorkeeper.pl.yml
+++ b/config/locales/doorkeeper.pl.yml
@@ -31,7 +31,7 @@ pl:
       help:
         native_redirect_uri: Użyj %{native_redirect_uri} do lokalnych testów
         redirect_uri: Jeden adres na linię tekstu
-        scopes: Rozdziel zakresy (scopes) spacjami. Zostaw puste aby użyć domyślnych zakresów.
+        scopes: Rozdziel zakresy (scopes) spacjami. Zostaw puste, aby użyć domyślnych zakresów.
       index:
         callback_url: URL wywołania zwrotnego (callback)
         name: Nazwa
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 90b4fe82b..1fa0de90b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -215,7 +215,7 @@ en:
       body: "%{reporter} has reported %{target}"
       subject: New report for %{instance} (#%{id})
   application_mailer:
-    salutation: '%{name},'
+    salutation: "%{name},"
     settings: 'Change e-mail preferences: %{link}'
     signature: Mastodon notifications from %{instance}
     view: 'View:'
@@ -228,6 +228,7 @@ en:
     delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
     didnt_get_confirmation: Didn't receive confirmation instructions?
     forgot_password: Forgot your password?
+    invalid_reset_password_token: Password reset token is invalid or expired. Please request a new one.
     login: Log in
     logout: Logout
     register: Sign up
@@ -243,7 +244,6 @@ en:
       close: Or, you can just close this window.
       return: Return to the user's profile
       web: Go to web
-    prompt_html: 'You (<strong>%{self}</strong>) have requested to follow:'
     title: Follow %{acct}
   datetime:
     distance_in_words:
@@ -523,3 +523,4 @@ en:
   users:
     invalid_email: The e-mail address is invalid
     invalid_otp_token: Invalid two-factor code
+    signed_in_as: 'Signed in as:'
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index 6673b6516..f8b5ec0ac 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -42,7 +42,6 @@ eo:
   authorize_follow:
     error: Bedaŭrinde, okazis eraro provante konsulti la foran konton
     follow: Sekvi
-    prompt_html: 'Vi (<strong>%{self}</strong>) petis sekvi:'
     title: Sekvi %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 89e2828d0..d2d1de14f 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -43,7 +43,6 @@ es:
   authorize_follow:
     error: Desafortunadamente, ha ocurrido un error buscando la cuenta remota
     follow: Seguir
-    prompt_html: 'Tú (<strong>%{self}</strong>) has solicitado seguir:'
     title: Seguir %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index eb66a9c41..0c575e23e 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -2,7 +2,7 @@
 fa:
   about:
     about_mastodon_html: ماستدون (Mastodon) یک شبکهٔ اجتماعی است که بر اساس پروتکل‌های آزاد وب و نرم‌افزارهای آزاد و کدباز ساخته شده است. این شبکه مانند ایمیل غیرمتمرکز است.
-    about_this: درباره.
+    about_this: درباره
     closed_registrations: ثبت‌نام روی این سرور هم‌اینک فعال نیست. اما شما می‌توانید سرور دیگری بیابید و با حسابی که آن‌جا می‌سازید دقیقاً به همین شبکه دسترسی داشته باشید.
     contact: تماس
     contact_missing: تعیین نشده
@@ -20,11 +20,11 @@ fa:
       not_a_product_title: شما یک انسان هستید، نه یک محصول
       real_conversation_body: با ۵۰۰ نویسه برای هر نوشته و با پشتیبانی از هشدارهای موردی برای نوشته‌ها و تصاویر، می‌توانید خود را همان گونه که می‌خواهید ابراز کنید.
       real_conversation_title: برای گفتگوهای واقعی
-      within_reach_body: اپ‌های متنوع برای iOS، اندروید، و سیستم‌های دیگر به خاطر وحود یک اکوسیستم API دوستانه برای برنامه‌نویسان. از همه جا با دوستان خود ارتباط داشته باشید.
+      within_reach_body: اپ‌های متنوع برای iOS، اندروید، و سیستم‌های دیگر به خاطر وجود یک اکوسیستم API دوستانه برای برنامه‌نویسان. از همه جا با دوستان خود ارتباط داشته باشید.
       within_reach_title: همیشه در دسترس
     find_another_instance: یافتن سرورهای دیگر
     generic_description: "%{domain} یک سرور روی شبکه است"
-    hosted_on: ماستدون میزبانی‌شده روی %{domain}
+    hosted_on: ماستدون، میزبانی‌شده روی %{domain}
     learn_more: بیشتر بدانید
     other_instances: فهرست سرورها
     source_code: کدهای منبع
@@ -212,10 +212,10 @@ fa:
     title: مدیریت
   admin_mailer:
     new_report:
-      body: "کاربر %{reporter} کاربر %{target} را گزارش داد"
+      body: کاربر %{reporter} کاربر %{target} را گزارش داد
       subject: گزارش تازه‌ای برای %{instance} (#%{id})
   application_mailer:
-    salutation: '%{name},'
+    salutation: "%{name},"
     settings: 'تغییر تنظیمات ایمیل: %{link}'
     signature: اعلان‌های ماستدون از %{instance}
     view: 'نمایش:'
@@ -243,7 +243,6 @@ fa:
       close: یا این پنجره را ببندید.
       return: به نمایهٔ این کاربر بازگردید
       web: رفتن به وب
-    prompt_html: 'شما (<strong>%{self}</strong>) می‌خواهید این حساب را پی بگیرید:'
     title: پیگیری %{acct}
   datetime:
     distance_in_words:
@@ -433,7 +432,7 @@ fa:
     sensitive_content: محتوای حساس
   terms:
     body_html: |
-      <h2>Privacy Policy</h2>
+      <h2>سیاست رازداری (Privacy Policy)</h2>
 
       <h3 id="collect">ما چه اطلاعاتی را گردآوری می‌کنیم؟</h3>
 
@@ -451,7 +450,7 @@ fa:
         <li>برای شخصی‌سازی تجربهٔ کاربری شما &mdash; ما به کمک اطلاعات شما بهتر می‌توانیم نیازهای شما را برآورده کنیم.</li>
         <li>برای بهتر کردن سایت &mdash; ما پیوسته می‌کوشیم تا خدمات این سایت را به کمک اطلاعات و بازخوردی که از شما می‌گیریم بهتر کنیم.</li>
         <li>برای بهتر کردن خدمات به کاربران &mdash; ما به کمک اطلاعات شما به طور مؤثرتری می‌توانیم به درخواست‌های پشتیبانی شما پاسخ دهیم.</li>
-        <li>برای فرستادن ایمیل‌های دوره‌ای &mdash; ما گاهی به نشانی ایمیلی که وارد کرده‌اید نامه می‌فرستیم تا درخواست‌های شما پاسخ دهیم یا شما را در جریان پاسخ دیگران به شما قرار دهیم.</li>
+        <li>برای فرستادن ایمیل‌های دوره‌ای &mdash; ما گاهی به نشانی ایمیلی که وارد کرده‌اید نامه می‌فرستیم تا به درخواست‌های شما پاسخ دهیم یا شما را در جریان پاسخ دیگران به شما قرار دهیم.</li>
       </ul>
 
       <h3 id="protect">ما چگونه از اطلاعات شما محافظت می‌کنیم؟</h3>
@@ -500,7 +499,7 @@ fa:
       <p>این نوشته تحت اجازه‌نامهٔ CC-BY-SA قرار دارد. تاریخ آخرین به‌روزرسانی آن ۱۰ خرداد ۱۳۹۲ است.</p>
 
       <p>این نوشته اقتباسی است از <a href="https://github.com/discourse/discourse">سیاست رازداری Discourse</a>.</p>
-    title: "شرایط استفاده و سیاست رازداری %{instance}"
+    title: شرایط استفاده و سیاست رازداری %{instance}
   time:
     formats:
       default: "%d %b %Y, %H:%M"
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 23c844741..b748f7184 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -42,7 +42,6 @@ fi:
   authorize_follow:
     error: Valitettavasti tapahtui virhe etätilin haussa.
     follow: Seuraa
-    prompt_html: 'Sinä (<strong>%{self}</strong>) olet pyytänyt lupaa seurata:'
     title: Seuraa %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 7fde60a2b..d7aa41497 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -220,6 +220,7 @@ fr:
     delete_account_html: Si vous désirez supprimer votre compte, vous pouvez cliquer ici. Il vous sera demandé de confirmer cette action.
     didnt_get_confirmation: Vous n’avez pas reçu les consignes de confirmation ?
     forgot_password: Mot de passe oublié ?
+    invalid_reset_password_token: Le lien de réinitialisation du mot de passe est invalide ou a expiré. Merci de réessayer.
     login: Se connecter
     logout: Se déconnecter
     register: S’inscrire
@@ -235,7 +236,6 @@ fr:
       close: Ou bien, vous pouvez fermer cette fenêtre.
       return: Retour au profil de l'utilisateur⋅trice
       web: Retour à l'interface web
-    prompt_html: 'Vous (<strong>%{self}</strong>) avez demandé à suivre :'
     title: Suivre %{acct}
   datetime:
     distance_in_words:
@@ -278,7 +278,7 @@ fr:
     blocks: Vous bloquez
     csv: CSV
     follows: Vous suivez
-    mutes: Vous faites taire
+    mutes: Vous masquez
     storage: Médias stockés
   followers:
     domain: Domaine
@@ -305,7 +305,7 @@ fr:
     types:
       blocking: Liste d’utilisateur⋅ice⋅s bloqué⋅es
       following: Liste d’utilisateur⋅ice⋅s suivi⋅es
-      muting: Liste d’utilisateur⋅ice⋅s que vous faites taire
+      muting: Liste d’utilisateur⋅ice⋅s que vous masquez
     upload: Importer
   landing_strip_html: <strong>%{name}</strong> utilise %{link_to_root_path}. Vous pouvez le/la suivre et interagir si vous possédez un compte quelque part dans le "fediverse".
   landing_strip_signup_html: Si ce n’est pas le cas, vous pouvez <a href="%{sign_up_path}">en créer un ici</a>.
@@ -451,3 +451,4 @@ fr:
   users:
     invalid_email: L’adresse courriel est invalide
     invalid_otp_token: Le code d’authentification à deux facteurs est invalide
+    signed_in_as: 'Connecté·e en tant que :'
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 7772e6a76..f04e8ad62 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -177,7 +177,6 @@ he:
   authorize_follow:
     error: למרבה הצער, היתה שגיאה בחיפוש החשבון המרוחק
     follow: לעקוב
-    prompt_html: 'בקשת מעקב ממך (<strong>%{self}</strong>) אחרי:'
     title: לעקוב אחרי %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/hr.yml b/config/locales/hr.yml
index 2d43fcad8..52a8bd35f 100644
--- a/config/locales/hr.yml
+++ b/config/locales/hr.yml
@@ -43,7 +43,6 @@ hr:
   authorize_follow:
     error: Nažalost, došlo je do greške looking up the remote račun
     follow: Slijedi
-    prompt_html: 'Ti si (<strong>%{self}</strong>) poslao zahtjev za sljeđenje:'
     title: Slijedi %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/id.yml b/config/locales/id.yml
index 0d5937cfb..c76b3d6bb 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -168,7 +168,6 @@ id:
   authorize_follow:
     error: Sayangnya, ada error saat melihat akun remote
     follow: Ikuti
-    prompt_html: 'Anda (<strong>%{self}</strong>) telah diminta untuk mengikuti:'
     title: Mengikuti %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/io.yml b/config/locales/io.yml
index c9abd5711..112771ee4 100644
--- a/config/locales/io.yml
+++ b/config/locales/io.yml
@@ -166,7 +166,6 @@ io:
   authorize_follow:
     error: Regretinde, eventis eraro probante konsultar la fora konto
     follow: Sequar
-    prompt_html: 'Tu (<strong>%{self}</strong>) demandis sequar:'
     title: Sequar %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/it.yml b/config/locales/it.yml
index de9682589..75d56362a 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -43,7 +43,6 @@ it:
   authorize_follow:
     error: Sfortunatamente c'è stato un errore nel consultare l'account remoto
     follow: Segui
-    prompt_html: 'Tu, (<strong>%{self}</strong>), hai richiesto di seguire:'
     title: Segui %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index fa8f4566c..05c712234 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -234,7 +234,7 @@ ja:
     reset_password: パスワードを再発行
     set_new_password: 新しいパスワード
   authorize_follow:
-    error: 残念ながら、リモートアカウントにエラーが発生しました。
+    error: 残念ながら、リモートアカウント情報の取得中にエラーが発生しました。
     follow: フォロー
     follow_request: 'あなたは以下のアカウントにフォローリクエストを送信しました:'
     following: '成功! あなたは現在以下のアカウントをフォローしています:'
@@ -242,7 +242,6 @@ ja:
       close: またはこのウィンドウを閉じます
       return: ユーザーのプロフィールに戻る
       web: Web を開く
-    prompt_html: 'あなた(<strong>%{self}</strong>)は以下のアカウントのフォローをリクエストしました:'
     title: "%{acct} をフォロー"
   datetime:
     distance_in_words:
@@ -522,3 +521,4 @@ ja:
   users:
     invalid_email: メールアドレスが無効です
     invalid_otp_token: 二段階認証コードが間違っています
+    signed_in_as: '下記でログイン中:'
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index aae0e62e7..f3bde5bbb 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -189,7 +189,6 @@ ko:
   authorize_follow:
     error: 리모트 팔로우 도중 오류가 발생했습니다.
     follow: 팔로우
-    prompt_html: '나(<strong>%{self}</strong>) 는 아래 계정의 팔로우를 요청했습니다:'
     title: "%{acct} 를 팔로우"
   datetime:
     distance_in_words:
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index e65658d8b..6562767a9 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -106,7 +106,7 @@ nl:
       domain: Domein
       new:
         create: Blokkade aanmaken
-        hint: Een domeinblokkade voorkomt niet dat accountgegevens van dit domein aan de database worden toegevoegd, maar dat er met terugwerkende kracht en automatisch bepaalde moderatiemethoden op deze accounts worden toegepast. 
+        hint: Een domeinblokkade voorkomt niet dat accountgegevens van dit domein aan de database worden toegevoegd, maar dat er met terugwerkende kracht en automatisch bepaalde moderatiemethoden op deze accounts worden toegepast.
         severity:
           desc_html: "<strong>Negeren</strong> zorgt ervoor dat berichten van accounts van dit domein voor iedereen onzichtbaar zijn, behalve als een account wordt gevolgd. <strong>Opschorten</strong> zorgt ervoor dat alle berichten, media en profielgegevens van accounts van dit domein worden verwijderd. Gebruik <strong>Geen</strong> wanneer je alleen mediabestanden wilt weigeren."
           noop: Geen
@@ -129,7 +129,7 @@ nl:
           suspend: Alle opgeschorste accounts van dit domein niet meer opschorten
         title: Domeinblokkade voor %{domain} ongedaan maken
         undo: Ongedaan maken
-      title: Domeinblokkades 
+      title: Domeinblokkades
       undo: Ongedaan maken
     instances:
       account_count: Bekende accounts
@@ -169,7 +169,7 @@ nl:
           title: Bericht wanneer registratie is uitgeschakeld
         deletion:
           desc_html: Toestaan dat iedereen hun eigen account kan verwijderen
-          title:  Verwijderen account toestaan
+          title: Verwijderen account toestaan
         open:
           desc_html: Toestaan dat iedereen een account kan registereren
           title: Open registratie
@@ -220,7 +220,7 @@ nl:
   applications:
     invalid_url: De opgegeven URL is ongeldig
   auth:
-    agreement_html: Wanneer je op registeren klikt ga je akkoord met <a href="%{rules_path}">onze gebruikersvoorwaarden</a> en <a href="%{terms_path}">ons privacybeleid</a>.
+    agreement_html: Wanneer je op registreren klikt ga je akkoord met <a href="%{rules_path}">onze gebruikersvoorwaarden</a> en <a href="%{terms_path}">ons privacybeleid</a>.
     change_password: Beveiliging
     delete_account: Account verwijderen
     delete_account_html: Wanneer je jouw account graag wilt verwijderen, kan je dat <a href="%{path}">hier doen</a>. We vragen jou daar om een bevestiging.
@@ -241,7 +241,6 @@ nl:
       close: Of je kan dit venster gewoon sluiten.
       return: Ga terug naar het profiel van de gebruiker
       web: Ga naar de webapp
-    prompt_html: 'Je (<strong>%{self}</strong>) hebt toestemming gevraagd om iemand te mogen volgen:'
     title: Volg %{acct}
   datetime:
     distance_in_words:
@@ -307,7 +306,7 @@ nl:
       following: Volglijst
       muting: Negeerlijst
     upload: Uploaden
-  landing_strip_html: <strong>%{name}</strong> is een gebruiker op %{link_to_root_path}. Je kunt deze volgen en ermee communiceren als je ergens in deze fediverse een account hebt.
+  landing_strip_html: "<strong>%{name}</strong> is een gebruiker op %{link_to_root_path}. Je kunt deze volgen en ermee communiceren als je ergens in deze fediverse een account hebt."
   landing_strip_signup_html: Als je dat niet hebt, kun je je <a href="%{sign_up_path}">hier registreren</a>.
   media_attachments:
     validations:
@@ -510,7 +509,7 @@ nl:
     generate_recovery_codes: Herstelcodes genereren
     instructions_html: "<strong>Scan deze QR-code in Google Authenticator of een soortgelijke app op jouw mobiele telefoon</strong>. Van nu af aan genereert deze app aanmeldcodes die je bij het aanmelden moet invoeren."
     lost_recovery_codes: Met herstelcodes kun je toegang tot jouw account krijgen wanneer je jouw telefoon bent kwijtgeraakt. Wanneer je jouw herstelcodes bent kwijtgeraakt, kan je ze hier opnieuw genereren. Jouw oude herstelcodes zijn daarna ongeldig.
-    manual_instructions: 'Hieronder vind je de geheime code in platte tekst. Voor het geval je de QR-code niet kunt scannen en het handmatig moet invoeren.'
+    manual_instructions: Hieronder vind je de geheime code in platte tekst. Voor het geval je de QR-code niet kunt scannen en het handmatig moet invoeren.
     recovery_codes: Herstelcodes back-uppen
     recovery_codes_regenerated: Opnieuw genereren herstelcodes geslaagd
     recovery_instructions_html: Wanneer je ooit de toegang verliest tot jouw telefoon, kan je met behulp van een van de herstelcodes hieronder opnieuw toegang krijgen tot jouw account. Zorg ervoor dat je de herstelcodes op een veilige plek bewaard. (Je kunt ze bijvoorbeeld printen en ze samen met andere belangrijke documenten bewaren.)
diff --git a/config/locales/no.yml b/config/locales/no.yml
index b2e5773de..996ea1d97 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -170,7 +170,6 @@
   authorize_follow:
     error: Uheldigvis så skjedde det en feil da vi prøvde å få tak i en bruker fra en annen instans.
     follow: Følg
-    prompt_html: 'Du (<strong>%{self}</strong>) har spurt om å følge:'
     title: Følg %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index d9a589287..6c3f95823 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -31,7 +31,7 @@ oc:
     status_count_after: estatuts
     status_count_before: qu’an escrich
     user_count_after: personas
-    user_count_before: Ostal de 
+    user_count_before: Ostal de
     what_is_mastodon: Qu’es Mastodon ?
   accounts:
     follow: Sègre
@@ -214,6 +214,7 @@ oc:
       body: "%{reporter} a senhalat %{target}"
       subject: Novèl senhalament per %{instance} (#%{id})
   application_mailer:
+    salutation: '%{name},'
     settings: 'Cambiar las preferéncias de corrièl : %{link}'
     signature: Notificacion de Mastodon sus %{instance}
     view: 'Veire :'
@@ -232,6 +233,7 @@ oc:
     resend_confirmation: Tornar mandar las instruccions de confirmacion
     reset_password: Reïnicializar lo senhal
     set_new_password: Picar un nòu senhal
+    invalid_reset_password_token: Ligam de reïnicializacion invalid o acabat. Tornatz ensajar se vos plai.
   authorize_follow:
     error: O planhèm, i a agut una error al moment de cercar lo compte
     follow: Sègre
@@ -241,7 +243,6 @@ oc:
       close: O podètz tampar aquesta fenèstra.
       return: Tornar al perfil
       web: Tornar a l’interfàcia Web
-    prompt_html: 'Avètz (<strong>%{self}</strong>) demandat de sègre :'
     title: Sègre %{acct}
   date:
     abbr_day_names:
@@ -579,3 +580,4 @@ oc:
   users:
     invalid_email: L’adreça de corrièl es invalida
     invalid_otp_token: Còdi d’autentificacion en dos temps invalid
+    signed_in_as: 'Session a'
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index a30092d50..415c3b993 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -109,12 +109,14 @@ pl:
         hint: Blokada domen nie zabroni tworzenia wpisów kont w bazie danych, ale pozwoli na automatyczną moderację kont do nich należących.
         severity:
           desc_html: "<strong>Wyciszenie</strong> uczyni wpisy użytkownika widoczne tylko dla osób, które go śledzą. <strong>Zawieszenie</strong> spowoduje usunięcie całej zawartości dodanej przez użytkownika."
+          noop: Nic nie rób
           silence: Wycisz
           suspend: Zawieś
         title: Nowa blokada domen
       reject_media: Odrzucaj pliki multimedialne
       reject_media_hint: Usuwa przechowywane lokalnie pliki multimedialne i nie pozwala na ich pobieranie. Nieprzydatne przy zawieszeniu
       severities:
+        noop: Nic nie rób
         silence: Wycisz
         suspend: Zawieś
       severity: Priorytet
@@ -175,8 +177,8 @@ pl:
         desc_html: Akapit wprowadzający, widoczny na stronie głównej i znacznikach meta. Możesz korzystać z tagów HTML, w szczególności <code>&lt;a&gt;</code> i <code>&lt;em&gt;</code>.
         title: Opis instancji
       site_description_extended:
-        desc_html: Dobre miejsce na zasady użytkowania, wprowadzenie i inne rzeczy, które wyróżniają tą instancję. Możesz korzystać z tagów HTML
-        title: Niestandrdowy opis strony
+        desc_html: Dobre miejsce na zasady użytkowania, wprowadzenie i inne rzeczy, które wyróżniają tę instancję. Możesz korzystać z tagów HTML
+        title: Niestandardowy opis strony
       site_terms:
         desc_html: Miejsce na własną politykę prywatności, zasady użytkowania i inne unormowania prawne. Możesz używać tagów HTML
         title: Niestandardowe zasady użytkowania
@@ -213,6 +215,7 @@ pl:
       body: Użytkownik %{reporter} zgłosił %{target}
       subject: Nowe zgłoszenie na %{instance} (#%{id})
   application_mailer:
+    salutation: "%{name},"
     settings: 'Zmień ustawienia powiadamiania: %{link}'
     signature: Powiadomienie Mastodona z instancji %{instance}
     view: 'Zobacz:'
@@ -225,6 +228,7 @@ pl:
     delete_account_html: Jeżeli chcesz usunąć konto, <a href="%{path}">przejdź tutaj</a>. Otrzymasz prośbę o potwierdzenie.
     didnt_get_confirmation: Nie otrzymałeś instrukcji weryfikacji?
     forgot_password: Nie pamiętasz hasła?
+    invalid_reset_password_token: Token do resetowania hasła jest nieprawidłowy lub utracił ważność. Spróbuj uzyskać nowy.
     login: Zaloguj się
     logout: Wyloguj się
     register: Rejestracja
@@ -237,10 +241,9 @@ pl:
     follow_request: 'Wysłano prośbę o pozwolenie na śledzenie:'
     following: 'Pomyślnie! Od teraz śledzisz:'
     post_follow:
-      close: Ewentualnie, możesz po prostu zamknąć tą stronę.
+      close: Ewentualnie, możesz po prostu zamknąć tę stronę.
       return: Powróć do strony użytkownika
       web: Przejdź do sieci
-    prompt_html: 'Ty (<strong>%{self}</strong>) chcesz śledzić:'
     title: Śledź %{acct}
   datetime:
     distance_in_words:
@@ -262,10 +265,10 @@ pl:
     description_html: Ta opcja usunie <strong>bezpowrotnie i nieodwracalnie</strong> całą zawartość konta i zdezaktywuje je. Twoja nazwa użytkownika pozostanie zarezerwowana, aby zapobiec nadużyciom.
     proceed: Usuń konto
     success_msg: Twoje konto zostało pomyślnie usunięte
-    warning_html: Możemy usunąć zawartość jedynie w obrębie tej instancji. Zawartość udostępniona publicznie pozostawia trwałe ślady. Serwery niepodłączone do sieci, bądź nieśledzące Twoich aktualizacji mogą zachować Twoje dane.
+    warning_html: Możemy usunąć zawartość jedynie w obrębie tej instancji. Zawartość udostępniona publicznie pozostawia trwałe ślady. Serwery niepodłączone do sieci bądź nieśledzące Twoich aktualizacji mogą zachować Twoje dane.
     warning_title: Dostępność usuniętej zawartości
   errors:
-    '403': Nie masz uprawnień, aby wyświetlić tą stronę.
+    '403': Nie masz uprawnień, aby wyświetlić tę stronę.
     '404': Strona, którą próbujesz odwiedzić, nie istnieje.
     '410': Strona, którą próbujesz odwiedzić, już nie istnieje.
     '422':
@@ -336,8 +339,8 @@ pl:
       body: "%{name} poprosił o możliwość śledzenia Cię"
       subject: 'Prośba o możliwość śledzenia: %{name}'
     mention:
-      body: "%{name} wspomniał Cię w:"
-      subject: "%{name} Cię wspomniał"
+      body: "%{name} wspomniał o Tobie w:"
+      subject: "%{name} wspomniał o Tobie"
     reblog:
       body: 'Twój wpis został podbity przez %{name}:'
       subject: Twój wpis został podbity przez %{name}
@@ -524,3 +527,4 @@ pl:
   users:
     invalid_email: Adres e-mail jest niepoprawny
     invalid_otp_token: Kod uwierzytelniający jest niepoprawny
+    signed_in_as: 'Zalogowany jako:'
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 68b1c549c..6dec2b50a 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -169,7 +169,6 @@ pt-BR:
   authorize_follow:
     error: Infelizmente houve um erro olhando uma conta remota
     follow: Seguir
-    prompt_html: 'Você (<strong>%{self}</strong>) pediu pra seguir:'
     title: Seguir %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 348f670b5..0156f0e95 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -162,7 +162,6 @@ ru:
   authorize_follow:
     error: К сожалению, при поиске удаленного аккаунта возникла ошибка
     follow: Подписаться
-    prompt_html: 'Вы (<strong>%{self}</strong>) запросили подписку:'
     title: Подписаться на %{acct}
   datetime:
     distance_in_words:
@@ -269,14 +268,14 @@ ru:
     truncate: "&hellip;"
   push_notifications:
     favourite:
-      title: "Ваш статус понравился %{name}"
+      title: Ваш статус понравился %{name}
     follow:
       title: "%{name} теперь подписан(а) на Вас"
     mention:
       action_boost: Продвинуть
       action_expand: Развернуть
       action_favourite: Нравится
-      title: "Вас упомянул(а) %{name}"
+      title: Вас упомянул(а) %{name}
     reblog:
       title: "%{name} продвинул(а) Ваш статус"
     subscribed:
@@ -351,7 +350,7 @@ ru:
     reblogged: продвинул(а)
     sensitive_content: Чувствительный контент
   terms:
-    title: "Условия обслуживания и политика конфиденциальности %{instance}"
+    title: Условия обслуживания и политика конфиденциальности %{instance}
   time:
     formats:
       default: "%b %d, %Y, %H:%M"
diff --git a/config/locales/simple_form.fa.yml b/config/locales/simple_form.fa.yml
index 71a9d6e1f..dd72a19bd 100644
--- a/config/locales/simple_form.fa.yml
+++ b/config/locales/simple_form.fa.yml
@@ -40,7 +40,7 @@ fa:
         setting_boost_modal: نمایش پیغام تأیید پیش از بازبوقیدن
         setting_default_privacy: حریم خصوصی نوشته‌ها
         setting_default_sensitive: همیشه تصاویر را به عنوان حساس علامت بزن
-        setting_delete_modal: پیش از پاک کردن یک بوق پیغام تأیید نشان بده
+        setting_delete_modal: پیش از پاک کردن یک نوشته پیغام تأیید نشان بده
         setting_noindex: درخواست از موتورهای جستجو برای لغو فهرست‌سازی
         setting_system_font_ui: به‌کاربردن قلم پیش‌فرض سیستم
         setting_unfollow_modal: نمایش پیغام تأیید پیش از لغو پیگیری دیگران
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 801f4886f..9d0887928 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -170,7 +170,6 @@ th:
   authorize_follow:
     error: Unfortunately, there was an error looking up the remote account
     follow: ติดตาม
-    prompt_html: 'คุณ (<strong>%{self}</strong>) ขอติดตาม:'
     title: ติดตาม %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index ac378090c..91ef9544c 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -169,7 +169,6 @@ tr:
   authorize_follow:
     error: Uzak hesap aranırken bir hata oluştu.
     follow: Takip et
-    prompt_html: 'Siz (<strong>%{self}</strong>) bu kullanıcıyı takip etmek istiyor musunuz?:'
     title: "%{acct}'i takip et"
   datetime:
     distance_in_words:
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 22fff6961..4d12ddf4e 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -160,7 +160,6 @@ uk:
   authorize_follow:
     error: На жаль, при пошуку віддаленого аккаунту виникла помилка
     follow: Підписатися
-    prompt_html: 'Ви (<strong>%{self}</strong>) запитали про підписку:'
     title: Підписатися на %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 5018b48b8..0672202a2 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -176,7 +176,6 @@ zh-CN:
   authorize_follow:
     error: 对不起,寻找这个跨站用户时出错
     follow: 关注
-    prompt_html: 你 (<strong>%{self}</strong>) 正准备关注︰
     title: 关注 %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index 40087ed53..9d6c74008 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -169,7 +169,6 @@ zh-HK:
   authorize_follow:
     error: 對不起,尋找這個跨站用戶的過程發生錯誤
     follow: 關注
-    prompt_html: 你 (<strong>%{self}</strong>) 正準備關注︰
     title: 關注 %{acct}
   datetime:
     distance_in_words:
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 0ea3457c7..7065acf9a 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -140,7 +140,6 @@ zh-TW:
   authorize_follow:
     error: 對不起,尋找這個跨站使用者的過程發生錯誤
     follow: 關注
-    prompt_html: 您 (<strong>%{self}</strong>) 正準備關注︰
     title: 關注 %{acct}
   datetime:
     distance_in_words:
diff --git a/config/webpack/loaders/babel.js b/config/webpack/loaders/babel.js
index 49b191d26..05ef8431c 100644
--- a/config/webpack/loaders/babel.js
+++ b/config/webpack/loaders/babel.js
@@ -1,3 +1,5 @@
+const { resolve } = require('path');
+
 module.exports = {
   test: /\.js$/,
   // include react-intl because transform-react-remove-prop-types needs to apply to it
@@ -9,5 +11,6 @@ module.exports = {
   options: {
     forceEnv: process.env.NODE_ENV || 'development',
     sourceRoot: 'app/javascript',
+    cacheDirectory: resolve(__dirname, '..', '..', '..', 'tmp', 'cache', 'babel-loader'),
   },
 };
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index aeb5492dc..381e9aac9 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -13,7 +13,7 @@ module Mastodon
     end
 
     def patch
-      0
+      1
     end
 
     def pre
diff --git a/spec/controllers/auth/passwords_controller_spec.rb b/spec/controllers/auth/passwords_controller_spec.rb
index 60b225efa..992d2e29d 100644
--- a/spec/controllers/auth/passwords_controller_spec.rb
+++ b/spec/controllers/auth/passwords_controller_spec.rb
@@ -3,6 +3,8 @@
 require 'rails_helper'
 
 describe Auth::PasswordsController, type: :controller do
+  include Devise::Test::ControllerHelpers
+
   describe 'GET #new' do
     it 'returns http success' do
       @request.env['devise.mapping'] = Devise.mappings[:user]
@@ -10,4 +12,27 @@ describe Auth::PasswordsController, type: :controller do
       expect(response).to have_http_status(:success)
     end
   end
+
+  describe 'GET #edit' do
+    let(:user) { Fabricate(:user) }
+
+    before do
+      request.env['devise.mapping'] = Devise.mappings[:user]
+      @token = user.send_reset_password_instructions
+    end
+
+    context 'with valid reset_password_token' do
+      it 'returns http success' do
+        get :edit, params: { reset_password_token: @token }
+        expect(response).to have_http_status(:success)
+      end
+    end
+
+    context 'with invalid reset_password_token' do
+      it 'redirects to #new' do
+        get :edit, params: { reset_password_token: 'some_invalid_value' }
+        expect(response).to redirect_to subject.new_password_path(subject.send(:resource_name))
+      end
+    end
+  end
 end
diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb
index 06fdbaabc..88f0a4734 100644
--- a/spec/controllers/auth/sessions_controller_spec.rb
+++ b/spec/controllers/auth/sessions_controller_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
         sign_in(user, scope: :user)
         delete :destroy
 
-        expect(response).to redirect_to(root_path)
+        expect(response).to redirect_to(new_user_session_path)
       end
     end
 
@@ -38,7 +38,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
         sign_in(user, scope: :user)
         delete :destroy
 
-        expect(response).to redirect_to(root_path)
+        expect(response).to redirect_to(new_user_session_path)
       end
     end
   end
diff --git a/spec/javascript/components/display_name.test.js b/spec/javascript/components/display_name.test.js
index ad9288d4d..ab484cf3e 100644
--- a/spec/javascript/components/display_name.test.js
+++ b/spec/javascript/components/display_name.test.js
@@ -9,19 +9,9 @@ describe('<DisplayName />', () => {
     const account = fromJS({
       username: 'bar',
       acct: 'bar@baz',
-      display_name: 'Foo',
+      display_name_html: '<p>Foo</p>',
     });
     const wrapper = render(<DisplayName account={account} />);
     expect(wrapper).to.have.text('Foo @bar@baz');
   });
-
-  it('renders the username + account name if display name is empty', () => {
-    const account = fromJS({
-      username: 'bar',
-      acct: 'bar@baz',
-      display_name: '',
-    });
-    const wrapper = render(<DisplayName account={account} />);
-    expect(wrapper).to.have.text('bar @bar@baz');
-  });
 });