about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorDavid Yip <yipdw@member.fsf.org>2017-12-15 12:20:56 -0600
committerDavid Yip <yipdw@member.fsf.org>2017-12-15 12:20:56 -0600
commit82b2e224a26765caf743b24844912faa2de9873a (patch)
tree1ee49dd3dbcef7867ad93f875262951612815851 /app
parent6abb0950c6f694607cdc6a0c564751400d52ad5a (diff)
parente0a573a2cef924fdfec2cd7ec96a712fe2de1511 (diff)
Merge branch 'gs-master' into prevent-local-only-federation
 Conflicts:
	db/schema.rb
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/accounts_controller.rb3
-rw-r--r--app/controllers/admin/custom_emojis_controller.rb4
-rw-r--r--app/controllers/api/base_controller.rb15
-rw-r--r--app/controllers/api/v1/accounts/lists_controller.rb20
-rw-r--r--app/controllers/api/v1/lists_controller.rb38
-rw-r--r--app/controllers/api/web/push_subscriptions_controller.rb2
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/concerns/rate_limit_headers.rb3
-rw-r--r--app/controllers/oauth/authorizations_controller.rb5
-rw-r--r--app/controllers/oauth/authorized_applications_controller.rb5
-rw-r--r--app/controllers/settings/flavours_controller.rb35
-rw-r--r--app/controllers/settings/migrations_controller.rb3
-rw-r--r--app/controllers/settings/preferences_controller.rb3
-rw-r--r--app/helpers/admin/filter_helper.rb9
-rw-r--r--app/helpers/settings_helper.rb1
-rw-r--r--app/javascript/core/settings.js4
-rw-r--r--app/javascript/flavours/glitch/components/status.js5
-rw-r--r--app/javascript/flavours/glitch/components/status_action_bar.js4
-rw-r--r--app/javascript/flavours/glitch/containers/status_container.js14
-rw-r--r--app/javascript/flavours/glitch/features/status/components/action_bar.js4
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js14
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/favourite_modal.js84
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_root.js4
-rw-r--r--app/javascript/flavours/glitch/images/glitch-preview.jpgbin0 -> 197277 bytes
-rw-r--r--app/javascript/flavours/glitch/locales/ar.js7
-rw-r--r--app/javascript/flavours/glitch/locales/bg.js7
-rw-r--r--app/javascript/flavours/glitch/locales/ca.js7
-rw-r--r--app/javascript/flavours/glitch/locales/de.js7
-rw-r--r--app/javascript/flavours/glitch/locales/en.js52
-rw-r--r--app/javascript/flavours/glitch/locales/eo.js7
-rw-r--r--app/javascript/flavours/glitch/locales/es.js7
-rw-r--r--app/javascript/flavours/glitch/locales/fa.js7
-rw-r--r--app/javascript/flavours/glitch/locales/fi.js7
-rw-r--r--app/javascript/flavours/glitch/locales/fr.js7
-rw-r--r--app/javascript/flavours/glitch/locales/he.js7
-rw-r--r--app/javascript/flavours/glitch/locales/hr.js7
-rw-r--r--app/javascript/flavours/glitch/locales/hu.js7
-rw-r--r--app/javascript/flavours/glitch/locales/id.js7
-rw-r--r--app/javascript/flavours/glitch/locales/io.js7
-rw-r--r--app/javascript/flavours/glitch/locales/it.js7
-rw-r--r--app/javascript/flavours/glitch/locales/ja.js55
-rw-r--r--app/javascript/flavours/glitch/locales/ko.js7
-rw-r--r--app/javascript/flavours/glitch/locales/nl.js7
-rw-r--r--app/javascript/flavours/glitch/locales/no.js7
-rw-r--r--app/javascript/flavours/glitch/locales/oc.js7
-rw-r--r--app/javascript/flavours/glitch/locales/pl.js48
-rw-r--r--app/javascript/flavours/glitch/locales/pt-BR.js7
-rw-r--r--app/javascript/flavours/glitch/locales/pt.js7
-rw-r--r--app/javascript/flavours/glitch/locales/ru.js7
-rw-r--r--app/javascript/flavours/glitch/locales/sv.js7
-rw-r--r--app/javascript/flavours/glitch/locales/th.js7
-rw-r--r--app/javascript/flavours/glitch/locales/tr.js7
-rw-r--r--app/javascript/flavours/glitch/locales/uk.js7
-rw-r--r--app/javascript/flavours/glitch/locales/zh-CN.js7
-rw-r--r--app/javascript/flavours/glitch/locales/zh-HK.js7
-rw-r--r--app/javascript/flavours/glitch/locales/zh-TW.js7
-rw-r--r--app/javascript/flavours/glitch/names.yml8
-rw-r--r--app/javascript/flavours/glitch/packs/common.js3
-rw-r--r--app/javascript/flavours/glitch/styles/admin.scss16
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss12
-rw-r--r--app/javascript/flavours/glitch/theme.yml12
-rw-r--r--app/javascript/flavours/glitch/util/initial_state.js1
-rw-r--r--app/javascript/flavours/vanilla/names.yml8
-rw-r--r--app/javascript/flavours/vanilla/theme.yml16
-rw-r--r--app/javascript/glitch/locales/en.json46
-rw-r--r--app/javascript/glitch/locales/ja.json44
-rw-r--r--app/javascript/glitch/locales/pl.json44
-rw-r--r--app/javascript/images/screenshot.jpgbin0 -> 239221 bytes
-rw-r--r--app/javascript/locales/locale-data/README.md (renamed from app/javascript/mastodon/locales/locale-data/README.md)0
-rw-r--r--app/javascript/locales/locale-data/oc.js (renamed from app/javascript/mastodon/locales/locale-data/oc.js)0
-rw-r--r--app/javascript/mastodon/actions/favourites.js6
-rw-r--r--app/javascript/mastodon/actions/push_notifications.js11
-rw-r--r--app/javascript/mastodon/components/avatar.js5
-rw-r--r--app/javascript/mastodon/components/avatar_overlay.js13
-rw-r--r--app/javascript/mastodon/features/compose/components/compose_form.js12
-rw-r--r--app/javascript/mastodon/features/compose/components/reply_indicator.js8
-rw-r--r--app/javascript/mastodon/features/compose/components/upload.js2
-rw-r--r--app/javascript/mastodon/features/favourited_statuses/index.js10
-rw-r--r--app/javascript/mastodon/features/list_timeline/index.js2
-rw-r--r--app/javascript/mastodon/features/standalone/hashtag_timeline/index.js12
-rw-r--r--app/javascript/mastodon/features/standalone/public_timeline/index.js12
-rw-r--r--app/javascript/mastodon/features/status/components/card.js12
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js8
-rw-r--r--app/javascript/mastodon/features/ui/components/video_modal.js1
-rw-r--r--app/javascript/mastodon/features/video/index.js65
-rw-r--r--app/javascript/mastodon/locales/ar.json36
-rw-r--r--app/javascript/mastodon/locales/bg.json2
-rw-r--r--app/javascript/mastodon/locales/ca.json20
-rw-r--r--app/javascript/mastodon/locales/de.json2
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json15
-rw-r--r--app/javascript/mastodon/locales/en.json7
-rw-r--r--app/javascript/mastodon/locales/eo.json2
-rw-r--r--app/javascript/mastodon/locales/es.json2
-rw-r--r--app/javascript/mastodon/locales/fa.json2
-rw-r--r--app/javascript/mastodon/locales/fi.json2
-rw-r--r--app/javascript/mastodon/locales/fr.json44
-rw-r--r--app/javascript/mastodon/locales/gl.json259
-rw-r--r--app/javascript/mastodon/locales/he.json2
-rw-r--r--app/javascript/mastodon/locales/hr.json2
-rw-r--r--app/javascript/mastodon/locales/hu.json2
-rw-r--r--app/javascript/mastodon/locales/id.json2
-rw-r--r--app/javascript/mastodon/locales/io.json2
-rw-r--r--app/javascript/mastodon/locales/it.json2
-rw-r--r--app/javascript/mastodon/locales/ja.json21
-rw-r--r--app/javascript/mastodon/locales/ko.json2
-rw-r--r--app/javascript/mastodon/locales/nl.json44
-rw-r--r--app/javascript/mastodon/locales/no.json2
-rw-r--r--app/javascript/mastodon/locales/oc.json62
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json54
-rw-r--r--app/javascript/mastodon/locales/pt.json34
-rw-r--r--app/javascript/mastodon/locales/ru.json2
-rw-r--r--app/javascript/mastodon/locales/sv.json2
-rw-r--r--app/javascript/mastodon/locales/th.json2
-rw-r--r--app/javascript/mastodon/locales/tr.json2
-rw-r--r--app/javascript/mastodon/locales/uk.json2
-rw-r--r--app/javascript/mastodon/locales/whitelist_gl.json2
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json58
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json2
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json2
-rw-r--r--app/javascript/mastodon/middleware/sounds.js2
-rw-r--r--app/javascript/mastodon/reducers/status_lists.js12
-rw-r--r--app/javascript/mastodon/settings.js46
-rw-r--r--app/javascript/mastodon/stream.js8
-rw-r--r--app/javascript/mastodon/web_push_subscription.js32
-rw-r--r--app/javascript/skins/vanilla/win95/common.scss (renamed from app/javascript/skins/vanilla/win95.scss)0
-rw-r--r--app/javascript/skins/vanilla/win95/names.yml4
-rw-r--r--app/javascript/styles/mastodon/components.scss576
-rw-r--r--app/javascript/styles/mastodon/rtl.scss26
-rw-r--r--app/lib/activitypub/activity/create.rb22
-rw-r--r--app/lib/ostatus/activity/creation.rb21
-rw-r--r--app/lib/provider_discovery.rb19
-rw-r--r--app/lib/status_filter.rb2
-rw-r--r--app/lib/themes.rb20
-rw-r--r--app/lib/user_settings_decorator.rb5
-rw-r--r--app/models/account.rb3
-rw-r--r--app/models/account_filter.rb2
-rw-r--r--app/models/custom_emoji_filter.rb2
-rw-r--r--app/models/list.rb8
-rw-r--r--app/models/preview_card.rb2
-rw-r--r--app/models/tag.rb2
-rw-r--r--app/models/user.rb2
-rw-r--r--app/serializers/initial_state_serializer.rb1
-rw-r--r--app/serializers/rest/account_serializer.rb8
-rw-r--r--app/services/account_search_service.rb2
-rw-r--r--app/services/fetch_link_card_service.rb58
-rw-r--r--app/services/fetch_remote_status_service.rb2
-rw-r--r--app/services/follow_service.rb2
-rw-r--r--app/services/resolve_remote_account_service.rb2
-rw-r--r--app/validators/status_pin_validator.rb8
-rw-r--r--app/views/admin/accounts/_account.html.haml17
-rw-r--r--app/views/admin/accounts/index.html.haml9
-rw-r--r--app/views/admin/accounts/show.html.haml3
-rw-r--r--app/views/admin/custom_emojis/_custom_emoji.html.haml2
-rw-r--r--app/views/admin/custom_emojis/index.html.haml14
-rwxr-xr-xapp/views/layouts/application.html.haml6
-rw-r--r--app/views/settings/flavours/show.html.haml19
-rw-r--r--app/views/settings/preferences/show.html.haml5
-rw-r--r--app/views/stream_entries/_detailed_status.html.haml20
-rw-r--r--app/views/stream_entries/show.html.haml3
-rw-r--r--app/views/tags/show.html.haml7
160 files changed, 1860 insertions, 870 deletions
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index e9a512e70..7428c3f22 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -89,7 +89,8 @@ module Admin
         :username,
         :display_name,
         :email,
-        :ip
+        :ip,
+        :staff
       )
     end
   end
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb
index 3fa2a0b72..ccab03de4 100644
--- a/app/controllers/admin/custom_emojis_controller.rb
+++ b/app/controllers/admin/custom_emojis_controller.rb
@@ -92,7 +92,9 @@ module Admin
     def filter_params
       params.permit(
         :local,
-        :remote
+        :remote,
+        :by_domain,
+        :shortcode
       )
     end
   end
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 7cfe8fe71..5983c0fbe 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -72,19 +72,4 @@ class Api::BaseController < ApplicationController
   def render_empty
     render json: {}, status: 200
   end
-
-  def set_maps(statuses) # rubocop:disable Style/AccessorMethodName
-    if current_account.nil?
-      @reblogs_map    = {}
-      @favourites_map = {}
-      @mutes_map      = {}
-      return
-    end
-
-    status_ids       = statuses.compact.flat_map { |s| [s.id, s.reblog_of_id] }.uniq
-    conversation_ids = statuses.compact.map(&:conversation_id).compact.uniq
-    @reblogs_map     = Status.reblogs_map(status_ids, current_account)
-    @favourites_map  = Status.favourites_map(status_ids, current_account)
-    @mutes_map       = Status.mutes_map(conversation_ids, current_account)
-  end
 end
diff --git a/app/controllers/api/v1/accounts/lists_controller.rb b/app/controllers/api/v1/accounts/lists_controller.rb
new file mode 100644
index 000000000..a7ba89ce2
--- /dev/null
+++ b/app/controllers/api/v1/accounts/lists_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class Api::V1::Accounts::ListsController < Api::BaseController
+  before_action -> { doorkeeper_authorize! :read }
+  before_action :require_user!
+  before_action :set_account
+
+  respond_to :json
+
+  def index
+    @lists = @account.lists.where(account: current_account)
+    render json: @lists, each_serializer: REST::ListSerializer
+  end
+
+  private
+
+  def set_account
+    @account = Account.find(params[:account_id])
+  end
+end
diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb
index 9437373bd..180a91d81 100644
--- a/app/controllers/api/v1/lists_controller.rb
+++ b/app/controllers/api/v1/lists_controller.rb
@@ -1,18 +1,14 @@
 # frozen_string_literal: true
 
 class Api::V1::ListsController < Api::BaseController
-  LISTS_LIMIT = 50
-
   before_action -> { doorkeeper_authorize! :read },    only: [:index, :show]
   before_action -> { doorkeeper_authorize! :write }, except: [:index, :show]
 
   before_action :require_user!
   before_action :set_list, except: [:index, :create]
 
-  after_action :insert_pagination_headers, only: :index
-
   def index
-    @lists = List.where(account: current_account).paginate_by_max_id(limit_param(LISTS_LIMIT), params[:max_id], params[:since_id])
+    @lists = List.where(account: current_account).all
     render json: @lists, each_serializer: REST::ListSerializer
   end
 
@@ -44,36 +40,4 @@ class Api::V1::ListsController < Api::BaseController
   def list_params
     params.permit(:title)
   end
-
-  def insert_pagination_headers
-    set_pagination_headers(next_path, prev_path)
-  end
-
-  def next_path
-    if records_continue?
-      api_v1_lists_url pagination_params(max_id: pagination_max_id)
-    end
-  end
-
-  def prev_path
-    unless @lists.empty?
-      api_v1_lists_url pagination_params(since_id: pagination_since_id)
-    end
-  end
-
-  def pagination_max_id
-    @lists.last.id
-  end
-
-  def pagination_since_id
-    @lists.first.id
-  end
-
-  def records_continue?
-    @lists.size == limit_param(LISTS_LIMIT)
-  end
-
-  def pagination_params(core_params)
-    params.permit(:limit).merge(core_params)
-  end
 end
diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb
index d66237feb..52e250d02 100644
--- a/app/controllers/api/web/push_subscriptions_controller.rb
+++ b/app/controllers/api/web/push_subscriptions_controller.rb
@@ -28,6 +28,8 @@ class Api::Web::PushSubscriptionsController < Api::BaseController
       },
     }
 
+    data.deep_merge!(params[:data]) if params[:data]
+
     web_subscription = ::Web::PushSubscription.create!(
       endpoint: params[:subscription][:endpoint],
       key_p256dh: params[:subscription][:keys][:p256dh],
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index c6d148c8c..3b2070f39 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -62,6 +62,7 @@ class ApplicationController < ActionController::Base
       pack: pack_name,
       preload: nil,
       skin: nil,
+      supported_locales: data['locales'],
     }
     if data['pack'][pack_name].is_a?(Hash)
       pack_data[:common] = nil if data['pack'][pack_name]['use_common'] == false
@@ -93,6 +94,7 @@ class ApplicationController < ActionController::Base
       pack: nil,
       preload: nil,
       skin: nil,
+      supported_locales: data['locales'],
     }
   end
 
diff --git a/app/controllers/concerns/rate_limit_headers.rb b/app/controllers/concerns/rate_limit_headers.rb
index 36cb91075..b79c558d8 100644
--- a/app/controllers/concerns/rate_limit_headers.rb
+++ b/app/controllers/concerns/rate_limit_headers.rb
@@ -44,7 +44,8 @@ module RateLimitHeaders
   end
 
   def api_throttle_data
-    request.env['rack.attack.throttle_data']['api']
+    most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] }
+    request.env['rack.attack.throttle_data'][most_limited_type]
   end
 
   def request_time
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index e9cdf9fa8..eb977510b 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -5,6 +5,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
 
   before_action :store_current_location
   before_action :authenticate_resource_owner!
+  before_action :set_pack
 
   include Localized
 
@@ -13,4 +14,8 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
   def store_current_location
     store_location_for(:user, request.url)
   end
+
+  def set_pack
+    use_pack 'auth'
+  end
 end
diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb
index 395fbc51b..f95d672ec 100644
--- a/app/controllers/oauth/authorized_applications_controller.rb
+++ b/app/controllers/oauth/authorized_applications_controller.rb
@@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
 
   before_action :store_current_location
   before_action :authenticate_resource_owner!
+  before_action :set_pack
 
   include Localized
 
@@ -13,4 +14,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
   def store_current_location
     store_location_for(:user, request.url)
   end
+
+  def set_pack
+    use_pack 'settings'
+  end
 end
diff --git a/app/controllers/settings/flavours_controller.rb b/app/controllers/settings/flavours_controller.rb
new file mode 100644
index 000000000..865d5a479
--- /dev/null
+++ b/app/controllers/settings/flavours_controller.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class Settings::FlavoursController < Settings::BaseController
+
+  def index
+    redirect_to action: 'show', flavour: current_flavour
+  end
+
+  def show
+    unless Themes.instance.flavours.include?(params[:flavour]) or params[:flavour] == current_flavour
+      redirect_to action: 'show', flavour: current_flavour
+    end
+
+    @listing = Themes.instance.flavours
+    @selected = params[:flavour]
+  end
+
+  def update
+    user_settings.update(user_settings_params(params[:flavour]).to_h)
+    redirect_to action: 'show', flavour: params[:flavour]
+  end
+
+  private
+
+  def user_settings
+    UserSettingsDecorator.new(current_user)
+  end
+
+  def user_settings_params(flavour)
+    params.require(:user).merge({ setting_flavour: flavour }).permit(
+      :setting_flavour,
+      :setting_skin
+    )
+  end
+end
diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb
index b18403a7f..bc6436b87 100644
--- a/app/controllers/settings/migrations_controller.rb
+++ b/app/controllers/settings/migrations_controller.rb
@@ -28,6 +28,7 @@ class Settings::MigrationsController < ApplicationController
   end
 
   def migration_account_changed?
-    current_account.moved_to_account_id != @migration.account&.id
+    current_account.moved_to_account_id != @migration.account&.id &&
+      current_account.id != @migration.account&.id
   end
 end
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 277f0f657..7cd1abe0c 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -33,13 +33,12 @@ class Settings::PreferencesController < Settings::BaseController
       :setting_default_sensitive,
       :setting_unfollow_modal,
       :setting_boost_modal,
+      :setting_favourite_modal,
       :setting_delete_modal,
       :setting_auto_play_gif,
       :setting_reduce_motion,
       :setting_system_font_ui,
       :setting_noindex,
-      :setting_flavour,
-      :setting_skin,
       notification_emails: %i(follow follow_request reblog favourite mention digest),
       interactions: %i(must_be_follower must_be_following)
     )
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 9443934b3..359c43d0e 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -1,11 +1,12 @@
 # frozen_string_literal: true
 
 module Admin::FilterHelper
-  ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze
-  REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
-  INVITE_FILTER = %i(available expired).freeze
+  ACCOUNT_FILTERS      = %i(local remote by_domain silenced suspended recent username display_name email ip staff).freeze
+  REPORT_FILTERS       = %i(resolved account_id target_account_id).freeze
+  INVITE_FILTER        = %i(available expired).freeze
+  CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
 
-  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER
+  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS
 
   def filter_link_to(text, link_to_params, link_class_params = link_to_params)
     new_url = filtered_url_for(link_to_params)
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index abce85812..1d4cb8a57 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -10,6 +10,7 @@ module SettingsHelper
     eo: 'Esperanto',
     es: 'Español',
     fa: 'فارسی',
+    gl: 'Galego',
     fi: 'Suomi',
     fr: 'Français',
     he: 'עברית',
diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js
index ada5fba2b..c9edcf197 100644
--- a/app/javascript/core/settings.js
+++ b/app/javascript/core/settings.js
@@ -37,7 +37,3 @@ delegate(document, '#account_header', 'change', ({ target }) => {
 
   header.style.backgroundImage = `url(${url})`;
 });
-
-delegate(document, '#user_setting_flavour, #user_setting_skin', 'change', ({ target }) => {
-  target.form.submit();
-});
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index b0d9e3757..b8a0fd180 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -58,6 +58,7 @@ export default class Status extends ImmutablePureComponent {
     'settings',
     'prepend',
     'boostModal',
+    'favouriteModal',
     'muted',
     'collapse',
     'notification',
@@ -204,8 +205,8 @@ export default class Status extends ImmutablePureComponent {
     this.props.onReply(this.props.status, this.context.router.history);
   }
 
-  handleHotkeyFavourite = () => {
-    this.props.onFavourite(this.props.status);
+  handleHotkeyFavourite = (e) => {
+    this.props.onFavourite(this.props.status, e);
   }
 
   handleHotkeyBoost = e => {
diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js
index 5a06782be..cb663e773 100644
--- a/app/javascript/flavours/glitch/components/status_action_bar.js
+++ b/app/javascript/flavours/glitch/components/status_action_bar.js
@@ -71,8 +71,8 @@ export default class StatusActionBar extends ImmutablePureComponent {
     });
   }
 
-  handleFavouriteClick = () => {
-    this.props.onFavourite(this.props.status);
+  handleFavouriteClick = (e) => {
+    this.props.onFavourite(this.props.status, e);
   }
 
   handleReblogClick = (e) => {
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index b753de7b3..c0b9b5800 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -20,7 +20,7 @@ import { initMuteModal } from 'flavours/glitch/actions/mutes';
 import { initReport } from 'flavours/glitch/actions/reports';
 import { openModal } from 'flavours/glitch/actions/modal';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
+import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@@ -78,11 +78,19 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }
   },
 
-  onFavourite (status) {
+  onModalFavourite (status) {
+    dispatch(favourite(status));
+  },
+
+  onFavourite (status, e) {
     if (status.get('favourited')) {
       dispatch(unfavourite(status));
     } else {
-      dispatch(favourite(status));
+      if (e.shiftKey || !favouriteModal) {
+        this.onModalFavourite(status);
+      } else {
+        dispatch(openModal('FAVOURITE', { status, onFavourite: this.onModalFavourite }));
+      }
     }
   },
 
diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js
index 4d660ee3c..3190fd0be 100644
--- a/app/javascript/flavours/glitch/features/status/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js
@@ -48,8 +48,8 @@ export default class ActionBar extends React.PureComponent {
     this.props.onReblog(this.props.status, e);
   }
 
-  handleFavouriteClick = () => {
-    this.props.onFavourite(this.props.status);
+  handleFavouriteClick = (e) => {
+    this.props.onFavourite(this.props.status, e);
   }
 
   handleDeleteClick = () => {
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index 93b0fe9d9..40ae380ab 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -30,7 +30,7 @@ import { openModal } from 'flavours/glitch/actions/modal';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { HotKeys } from 'react-hotkeys';
-import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
+import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
 import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
 
 const messages = defineMessages({
@@ -95,11 +95,19 @@ export default class Status extends ImmutablePureComponent {
     }
   };
 
-  handleFavouriteClick = (status) => {
+  handleModalFavourite = (status) => {
+    this.props.dispatch(favourite(status));
+  }
+
+  handleFavouriteClick = (status, e) => {
     if (status.get('favourited')) {
       this.props.dispatch(unfavourite(status));
     } else {
-      this.props.dispatch(favourite(status));
+      if (e.shiftKey || !favouriteModal) {
+        this.handleModalFavourite(status);
+      } else {
+        this.props.dispatch(openModal('FAVOURITE', { status, onFavourite: this.handleModalFavourite }));
+      }
     }
   }
 
diff --git a/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js b/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js
new file mode 100644
index 000000000..70722411d
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js
@@ -0,0 +1,84 @@
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import Button from 'flavours/glitch/components/button';
+import StatusContent from 'flavours/glitch/components/status_content';
+import Avatar from 'flavours/glitch/components/avatar';
+import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
+import DisplayName from 'flavours/glitch/components/display_name';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+const messages = defineMessages({
+  favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
+});
+
+@injectIntl
+export default class FavouriteModal extends ImmutablePureComponent {
+
+  static contextTypes = {
+    router: PropTypes.object,
+  };
+
+  static propTypes = {
+    status: ImmutablePropTypes.map.isRequired,
+    onFavourite: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  componentDidMount() {
+    this.button.focus();
+  }
+
+  handleFavourite = () => {
+    this.props.onFavourite(this.props.status);
+    this.props.onClose();
+  }
+
+  handleAccountClick = (e) => {
+    if (e.button === 0) {
+      e.preventDefault();
+      this.props.onClose();
+      this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
+    }
+  }
+
+  setRef = (c) => {
+    this.button = c;
+  }
+
+  render () {
+    const { status, intl } = this.props;
+
+    return (
+      <div className='modal-root__modal favourite-modal'>
+        <div className='favourite-modal__container'>
+          <div className='status light'>
+            <div className='favourite-modal__status-header'>
+              <div className='favourite-modal__status-time'>
+                <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
+              </div>
+
+              <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
+                <div className='status__avatar'>
+                  <Avatar account={status.get('account')} size={48} />
+                </div>
+
+                <DisplayName account={status.get('account')} />
+              </a>
+            </div>
+
+            <StatusContent status={status} />
+          </div>
+        </div>
+
+        <div className='favourite-modal__action-bar'>
+          <div><FormattedMessage id='favourite_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-star' /></span> }} /></div>
+          <Button text={intl.formatMessage(messages.favourite)} onClick={this.handleFavourite} ref={this.setRef} />
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
index 679578b75..a3e734867 100644
--- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
@@ -7,6 +7,7 @@ import ActionsModal from './actions_modal';
 import MediaModal from './media_modal';
 import VideoModal from './video_modal';
 import BoostModal from './boost_modal';
+import FavouriteModal from './favourite_modal';
 import DoodleModal from './doodle_modal';
 import ConfirmationModal from './confirmation_modal';
 import {
@@ -23,6 +24,7 @@ const MODAL_COMPONENTS = {
   'ONBOARDING': OnboardingModal,
   'VIDEO': () => Promise.resolve({ default: VideoModal }),
   'BOOST': () => Promise.resolve({ default: BoostModal }),
+  'FAVOURITE': () => Promise.resolve({ default: FavouriteModal }),
   'DOODLE': () => Promise.resolve({ default: DoodleModal }),
   'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
   'MUTE': MuteModal,
@@ -92,7 +94,7 @@ export default class ModalRoot extends React.PureComponent {
   }
 
   renderLoading = modalId => () => {
-    return ['MEDIA', 'VIDEO', 'BOOST', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
+    return ['MEDIA', 'VIDEO', 'BOOST', 'FAVOURITE', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
   }
 
   renderError = (props) => {
diff --git a/app/javascript/flavours/glitch/images/glitch-preview.jpg b/app/javascript/flavours/glitch/images/glitch-preview.jpg
new file mode 100644
index 000000000..fc5c42043
--- /dev/null
+++ b/app/javascript/flavours/glitch/images/glitch-preview.jpg
Binary files differdiff --git a/app/javascript/flavours/glitch/locales/ar.js b/app/javascript/flavours/glitch/locales/ar.js
new file mode 100644
index 000000000..1081147d5
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/ar.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/ar.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/bg.js b/app/javascript/flavours/glitch/locales/bg.js
new file mode 100644
index 000000000..979039376
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/bg.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/bg.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/ca.js b/app/javascript/flavours/glitch/locales/ca.js
new file mode 100644
index 000000000..baf76bd6f
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/ca.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/ca.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/de.js b/app/javascript/flavours/glitch/locales/de.js
new file mode 100644
index 000000000..ce6453623
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/de.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/de.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/en.js b/app/javascript/flavours/glitch/locales/en.js
new file mode 100644
index 000000000..0681d27d8
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/en.js
@@ -0,0 +1,52 @@
+import inherited from 'mastodon/locales/en.json';
+
+const messages = {
+  'getting_started.open_source_notice': 'Glitchsoc is free open source software forked from {Mastodon}. You can contribute or report issues on GitHub at {github}.',
+  'layout.auto': 'Auto',
+  'layout.current_is': 'Your current layout is:',
+  'layout.desktop': 'Desktop',
+  'layout.mobile': 'Mobile',
+  'navigation_bar.app_settings': 'App settings',
+  'getting_started.onboarding': 'Show me around',
+  'onboarding.page_one.federation': '{domain} is an \'instance\' of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.',
+  'onboarding.page_one.welcome': 'Welcome to {domain}!',
+  'onboarding.page_six.github': '{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}, and is compatible with any Mastodon instance or app. Glitchsoc is entirely free and open-source. You can report bugs, request features, or contribute to the code on {github}.',
+  'settings.auto_collapse': 'Automatic collapsing',
+  'settings.auto_collapse_all': 'Everything',
+  'settings.auto_collapse_lengthy': 'Lengthy toots',
+  'settings.auto_collapse_media': 'Toots with media',
+  'settings.auto_collapse_notifications': 'Notifications',
+  'settings.auto_collapse_reblogs': 'Boosts',
+  'settings.auto_collapse_replies': 'Replies',
+  'settings.close': 'Close',
+  'settings.collapsed_statuses': 'Collapsed toots',
+  'settings.enable_collapsed': 'Enable collapsed toots',
+  'settings.general': 'General',
+  'settings.image_backgrounds': 'Image backgrounds',
+  'settings.image_backgrounds_media': 'Preview collapsed toot media',
+  'settings.image_backgrounds_users': 'Give collapsed toots an image background',
+  'settings.media': 'Media',
+  'settings.media_letterbox': 'Letterbox media',
+  'settings.media_fullwidth': 'Full-width media previews',
+  'settings.preferences': 'User preferences',
+  'settings.wide_view': 'Wide view (Desktop mode only)',
+  'settings.navbar_under': 'Navbar at the bottom (Mobile only)',
+  'status.collapse': 'Collapse',
+  'status.uncollapse': 'Uncollapse',
+
+  'favourite_modal.combo': 'You can press {combo} to skip this next time',
+
+  'home.column_settings.show_direct': 'Show DMs',
+
+  'notification.markForDeletion': 'Mark for deletion',
+  'notifications.clear': 'Clear all my notifications',
+  'notifications.marked_clear_confirmation': 'Are you sure you want to permanently clear all selected notifications?',
+  'notifications.marked_clear': 'Clear selected notifications',
+
+  'notification_purge.btn_all': 'Select\nall',
+  'notification_purge.btn_none': 'Select\nnone',
+  'notification_purge.btn_invert': 'Invert\nselection',
+  'notification_purge.btn_apply': 'Clear\nselected',
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/eo.js b/app/javascript/flavours/glitch/locales/eo.js
new file mode 100644
index 000000000..04192f506
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/eo.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/eo.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/es.js b/app/javascript/flavours/glitch/locales/es.js
new file mode 100644
index 000000000..456df3c47
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/es.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/es.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/fa.js b/app/javascript/flavours/glitch/locales/fa.js
new file mode 100644
index 000000000..d82461a1a
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/fa.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/fa.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/fi.js b/app/javascript/flavours/glitch/locales/fi.js
new file mode 100644
index 000000000..11c3cd082
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/fi.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/fi.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/fr.js b/app/javascript/flavours/glitch/locales/fr.js
new file mode 100644
index 000000000..8562f5594
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/fr.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/fr.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/he.js b/app/javascript/flavours/glitch/locales/he.js
new file mode 100644
index 000000000..99516ee0c
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/he.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/he.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/hr.js b/app/javascript/flavours/glitch/locales/hr.js
new file mode 100644
index 000000000..dbf9b4b9f
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/hr.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/hr.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/hu.js b/app/javascript/flavours/glitch/locales/hu.js
new file mode 100644
index 000000000..1f0849af3
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/hu.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/hu.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/id.js b/app/javascript/flavours/glitch/locales/id.js
new file mode 100644
index 000000000..07e5f7e56
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/id.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/id.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/io.js b/app/javascript/flavours/glitch/locales/io.js
new file mode 100644
index 000000000..74ea6fae6
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/io.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/io.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/it.js b/app/javascript/flavours/glitch/locales/it.js
new file mode 100644
index 000000000..90f543093
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/it.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/it.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/ja.js b/app/javascript/flavours/glitch/locales/ja.js
new file mode 100644
index 000000000..f8a0be873
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/ja.js
@@ -0,0 +1,55 @@
+import inherited from 'mastodon/locales/ja.json';
+
+const messages = {
+  'getting_started.open_source_notice': 'Glitchsocは{Mastodon}によるフリーなオープンソースソフトウェアです。誰でもGitHub({github})から開発に參加したり、問題を報告したりできます。',
+  'layout.auto': '自動',
+  'layout.current_is': 'あなたの現在のレイアウト:',
+  'layout.desktop': 'Desktop',
+  'layout.mobile': 'Mobile',
+  'navigation_bar.app_settings': 'アプリ設定',
+  'getting_started.onboarding': '解説を表示',
+  'onboarding.page_one.federation': '{domain}はMastodonのインスタンスです。Mastodonとは、独立したサーバが連携して作るソーシャルネットワークです。これらのサーバーをインスタンスと呼びます。',
+  'onboarding.page_one.welcome': '{domain}へようこそ!',
+  'onboarding.page_six.github': '{domain}はGlitchsocを使用しています。Glitchsocは{Mastodon}のフレンドリーな{fork}で、どんなMastodonアプリやインスタンスとも互換性があります。Glitchsocは完全に無料で、オープンソースです。{github}でバグ報告や機能要望あるいは貢獻をすることが可能です。',
+  'settings.auto_collapse': '自動折りたたみ',
+  'settings.auto_collapse_all': 'すべて',
+  'settings.auto_collapse_lengthy': '長いトゥート',
+  'settings.auto_collapse_media': 'メディア付きトゥート',
+  'settings.auto_collapse_notifications': '通知',
+  'settings.auto_collapse_reblogs': 'ブースト',
+  'settings.auto_collapse_replies': '返信',
+  'settings.close': '閉じる',
+  'settings.collapsed_statuses': 'トゥート',
+  'settings.enable_collapsed': 'トゥート折りたたみを有効にする',
+  'settings.general': '一般',
+  'settings.image_backgrounds': '画像背景',
+  'settings.image_backgrounds_media': '折りたまれたメディア付きトゥートをプレビュー',
+  'settings.image_backgrounds_users': '折りたまれたトゥートの背景を変更する',
+  'settings.media': 'メディア',
+  'settings.media_letterbox': 'メディアをレターボックス式で表示',
+  'settings.media_fullwidth': '全幅メディアプレビュー',
+  'settings.preferences': 'ユーザー設定',
+  'settings.wide_view': 'ワイドビュー(Desktopレイアウトのみ)',
+  'settings.navbar_under': 'ナビを画面下部に移動させる(Mobileレイアウトのみ)',
+  'settings.compose_box_opts': 'コンポーズボックス設定',
+  'settings.side_arm': 'セカンダリートゥートボタン',
+  'settings.layout': 'レイアウト',
+  'status.collapse': '折りたたむ',
+  'status.uncollapse': '折りたたみを解除',
+
+  'favourite_modal.combo': '次からは {combo} を押せば、これをスキップできます。',
+
+  'home.column_settings.show_direct': 'DMを表示',
+
+  'notification.markForDeletion': '選択',
+  'notifications.clear': '通知を全てクリアする',
+  'notifications.marked_clear_confirmation': '削除した全ての通知を完全に削除してもよろしいですか?',
+  'notifications.marked_clear': '選択した通知を削除する',
+
+  'notification_purge.btn_all': 'すべて\n選択',
+  'notification_purge.btn_none': '選択\n解除',
+  'notification_purge.btn_invert': '選択を\n反転',
+  'notification_purge.btn_apply': '選択したものを\n削除',
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/ko.js b/app/javascript/flavours/glitch/locales/ko.js
new file mode 100644
index 000000000..3b55f89b9
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/ko.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/ko.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/nl.js b/app/javascript/flavours/glitch/locales/nl.js
new file mode 100644
index 000000000..17c371c58
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/nl.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/nl.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/no.js b/app/javascript/flavours/glitch/locales/no.js
new file mode 100644
index 000000000..794b1da25
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/no.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/no.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/oc.js b/app/javascript/flavours/glitch/locales/oc.js
new file mode 100644
index 000000000..8f161fd8c
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/oc.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/oc.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/pl.js b/app/javascript/flavours/glitch/locales/pl.js
new file mode 100644
index 000000000..818436710
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/pl.js
@@ -0,0 +1,48 @@
+import inherited from 'mastodon/locales/pl.json';
+
+const messages = {
+  'getting_started.open_source_notice': 'Glitchsoc jest wolnym i otwartoźródłowym forkiem oprogramowania {Mastodon}. Możesz współtworzyć projekt lub zgłaszać błędy na GitHubie pod adresem {github}.',
+  'layout.auto': 'Automatyczny',
+  'layout.current_is': 'Twój obecny układ to:',
+  'layout.desktop': 'Desktopowy',
+  'layout.mobile': 'Mobilny',
+  'navigation_bar.app_settings': 'Ustawienia aplikacji',
+  'getting_started.onboarding': 'Rozejrzyj się',
+  'onboarding.page_one.federation': '{domain} jest \'instancją\' Mastodona. Mastodon to sieć działających niezależnie serwerów tworzących jedną sieć społecznościową. Te serwery nazywane są instancjami.',
+  'onboarding.page_one.welcome': 'Witamy na {domain}!',
+  'onboarding.page_six.github': '{domain} jest oparty na Glitchsoc. Glitchsoc jest {forkiem} {Mastodon}a kompatybilnym z każdym klientem i aplikacją Mastodona. Glitchsoc jest całkowicie wolnym i otwartoźródłowym oprogramowaniem. Możesz zgłaszać błędy i sugestie funkcji oraz współtworzyć projekt na {github}.',
+  'settings.auto_collapse': 'Automatyczne zwijanie',
+  'settings.auto_collapse_all': 'Wszystko',
+  'settings.auto_collapse_lengthy': 'Długie wpisy',
+  'settings.auto_collapse_media': 'Wpisy z zawartością multimedialną',
+  'settings.auto_collapse_notifications': 'Powiadomienia',
+  'settings.auto_collapse_reblogs': 'Podbicia',
+  'settings.auto_collapse_replies': 'Odpowiedzi',
+  'settings.close': 'Zamknij',
+  'settings.collapsed_statuses': 'Zwijanie wpisów',
+  'settings.enable_collapsed': 'Włącz zwijanie wpisów',
+  'settings.general': 'Ogólne',
+  'settings.image_backgrounds': 'Obrazy w tle',
+  'settings.image_backgrounds_media': 'Wyświetlaj zawartość multimedialną zwiniętych wpisów',
+  'settings.image_backgrounds_users': 'Nadaj tło zwiniętym wpisom',
+  'settings.media': 'Zawartość multimedialna',
+  'settings.media_letterbox': 'Letterbox media',
+  'settings.media_fullwidth': 'Podgląd zawartości multimedialnej o pełnej szerokości',
+  'settings.preferences': 'Preferencje użyytkownika',
+  'settings.wide_view': 'Szeroki widok (tylko w trybie desktopowym)',
+  'settings.navbar_under': 'Pasek nawigacji na dole (tylko w trybie mobilnym)',
+  'status.collapse': 'Zwiń',
+  'status.uncollapse': 'Rozwiń',
+
+  'notification.markForDeletion': 'Oznacz do usunięcia',
+  'notifications.clear': 'Wyczyść wszystkie powiadomienia',
+  'notifications.marked_clear_confirmation': 'Czy na pewno chcesz bezpowrtonie usunąć wszystkie powiadomienia?',
+  'notifications.marked_clear': 'Usuń zaznaczone powiadomienia',
+
+  'notification_purge.btn_all': 'Zaznacz\nwszystkie',
+  'notification_purge.btn_none': 'Odznacz\nwszystkie',
+  'notification_purge.btn_invert': 'Odwróć\nzaznaczenie',
+  'notification_purge.btn_apply': 'Usuń\nzaznaczone',
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/pt-BR.js b/app/javascript/flavours/glitch/locales/pt-BR.js
new file mode 100644
index 000000000..6fed635f8
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/pt-BR.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/pt-BR.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/pt.js b/app/javascript/flavours/glitch/locales/pt.js
new file mode 100644
index 000000000..0156f55ff
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/pt.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/pt.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/ru.js b/app/javascript/flavours/glitch/locales/ru.js
new file mode 100644
index 000000000..0e9f1de71
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/ru.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/ru.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/sv.js b/app/javascript/flavours/glitch/locales/sv.js
new file mode 100644
index 000000000..b62c353fe
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/sv.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/sv.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/th.js b/app/javascript/flavours/glitch/locales/th.js
new file mode 100644
index 000000000..e939f8631
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/th.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/th.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/tr.js b/app/javascript/flavours/glitch/locales/tr.js
new file mode 100644
index 000000000..c2b740617
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/tr.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/tr.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/uk.js b/app/javascript/flavours/glitch/locales/uk.js
new file mode 100644
index 000000000..ab6d9a7dc
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/uk.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/uk.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/zh-CN.js b/app/javascript/flavours/glitch/locales/zh-CN.js
new file mode 100644
index 000000000..944588e02
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/zh-CN.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/zh-CN.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/zh-HK.js b/app/javascript/flavours/glitch/locales/zh-HK.js
new file mode 100644
index 000000000..b71c81f2b
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/zh-HK.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/zh-HK.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/locales/zh-TW.js b/app/javascript/flavours/glitch/locales/zh-TW.js
new file mode 100644
index 000000000..de2b7769c
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/zh-TW.js
@@ -0,0 +1,7 @@
+import inherited from 'mastodon/locales/zh-TW.json';
+
+const messages = {
+  //  No translations available.
+};
+
+export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/names.yml b/app/javascript/flavours/glitch/names.yml
new file mode 100644
index 000000000..ef82abed2
--- /dev/null
+++ b/app/javascript/flavours/glitch/names.yml
@@ -0,0 +1,8 @@
+en:
+  flavours:
+    glitch:
+      description: The default flavour for GlitchSoc instances.
+      name: Glitch Edition
+  skins:
+    glitch:
+      default: Default
diff --git a/app/javascript/flavours/glitch/packs/common.js b/app/javascript/flavours/glitch/packs/common.js
index 07445d2b3..8dd4372bc 100644
--- a/app/javascript/flavours/glitch/packs/common.js
+++ b/app/javascript/flavours/glitch/packs/common.js
@@ -1 +1,4 @@
 import 'flavours/glitch/styles/index.scss';
+
+//  This ensures that webpack compiles our images.
+require.context('../images', true);
diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 87bc710af..7c5032217 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -246,6 +246,22 @@
   }
 }
 
+.flavour-screen {
+  display: block;
+  margin: 10px auto;
+  max-width: 100%;
+}
+
+.flavour-description {
+  display: block;
+  font-size: 16px;
+  margin: 10px 0;
+
+  & > p {
+    margin: 10px 0;
+  }
+}
+
 .report-accounts {
   display: flex;
   flex-wrap: wrap;
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index e171b72c1..8f98863d8 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -2286,7 +2286,6 @@
 .getting-started {
   box-sizing: border-box;
   padding-bottom: 235px;
-  background: url('~images/mastodon-getting-started.png') no-repeat 0 100%;
   flex: 1 0 auto;
 
   p {
@@ -3913,6 +3912,7 @@ button.icon-button.active i.fa-retweet {
 }
 
 .boost-modal,
+.favourite-modal,
 .confirmation-modal,
 .report-modal,
 .actions-modal,
@@ -3944,7 +3944,8 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
-.boost-modal__container {
+.boost-modal__container,
+.favourite-modal__container{
   overflow-x: scroll;
   padding: 10px;
 
@@ -3955,6 +3956,7 @@ button.icon-button.active i.fa-retweet {
 }
 
 .boost-modal__action-bar,
+.favourite-modal__action-bar,
 .confirmation-modal__action-bar,
 .mute-modal__action-bar,
 .report-modal__action-bar {
@@ -3976,11 +3978,13 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
-.boost-modal__status-header {
+.boost-modal__status-header,
+.favourite-modal__status-header {
   font-size: 15px;
 }
 
-.boost-modal__status-time {
+.boost-modal__status-time,
+.favourite-modal__status-time {
   float: right;
   font-size: 14px;
 }
diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml
index fe09fa105..435fa2329 100644
--- a/app/javascript/flavours/glitch/theme.yml
+++ b/app/javascript/flavours/glitch/theme.yml
@@ -20,6 +20,18 @@ pack:
   settings:
   share: packs/share.js
 
+#  (OPTIONAL) The directory which contains localization files for
+#  the flavour, relative to this directory. The contents of this
+#  directory must be `.js` or `.json` files whose names correspond to
+#  language tags and whose default exports are a messages object.
+locales: locales
+
+#  (OPTIONAL) A file to use as the preview screenshot for the flavour,
+#  or an array thereof. These filenames must be unique across all
+#  images (regardless of path), so it's a good idea to namespace them
+#  to your theme. It's up to you to let webpack know to compile them.
+screenshot: glitch-preview.jpg
+
 #  (OPTIONAL) The directory which contains the pack files.
 #  Defaults to the theme directory (`app/javascript/themes/[theme]`),
 #  which should be sufficient for like 99% of use-cases lol.
diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js
index ef5d8b0ef..607d6b9b0 100644
--- a/app/javascript/flavours/glitch/util/initial_state.js
+++ b/app/javascript/flavours/glitch/util/initial_state.js
@@ -15,6 +15,7 @@ export const reduceMotion = getMeta('reduce_motion');
 export const autoPlayGif = getMeta('auto_play_gif');
 export const unfollowModal = getMeta('unfollow_modal');
 export const boostModal = getMeta('boost_modal');
+export const favouriteModal = getMeta('favourite_modal');
 export const deleteModal = getMeta('delete_modal');
 export const me = getMeta('me');
 
diff --git a/app/javascript/flavours/vanilla/names.yml b/app/javascript/flavours/vanilla/names.yml
new file mode 100644
index 000000000..94326f6ee
--- /dev/null
+++ b/app/javascript/flavours/vanilla/names.yml
@@ -0,0 +1,8 @@
+en:
+  flavours:
+    vanilla:
+      description: The theme used by vanilla Mastodon instances. This theme might not support all of the features of GlitchSoc.
+      name: Vanilla Mastodon
+  skins:
+    vanilla:
+      default: Default
diff --git a/app/javascript/flavours/vanilla/theme.yml b/app/javascript/flavours/vanilla/theme.yml
index 67fd9723e..0b27c31bb 100644
--- a/app/javascript/flavours/vanilla/theme.yml
+++ b/app/javascript/flavours/vanilla/theme.yml
@@ -20,13 +20,23 @@ pack:
   settings:
   share: share.js
 
+#  (OPTIONAL) The directory which contains localization files for
+#  the flavour, relative to this directory.
+locales: ../../mastodon/locales
+
+#  (OPTIONAL) A file to use as the preview screenshot for the flavour,
+#  or an array thereof. These filenames must be unique across all
+#  images (regardless of path), so it's a good idea to namespace them
+#  to your theme. It's up to you to let webpack know to compile them.
+screenshot: screenshot.jpg
+
 #  (OPTIONAL) The directory which contains the pack files.
-#  Defaults to the theme directory (`app/javascript/themes/[theme]`),
-#  but in the case of the vanilla Mastodon theme the pack files are
+#  Defaults to this directory (`app/javascript/flavour/[flavour]`),
+#  but in the case of the vanilla Mastodon flavour the pack files are
 #  somewhere else.
 pack_directory: app/javascript/packs
 
-#  (OPTIONAL) By default the theme will fallback to the default theme
+#  (OPTIONAL) By default the theme will fallback to the default flavour
 #  if a particular pack is not provided. You can specify different
 #  fallbacks here, or disable fallback behaviours altogether by
 #  specifying a `null` value.
diff --git a/app/javascript/glitch/locales/en.json b/app/javascript/glitch/locales/en.json
deleted file mode 100644
index 0276cb837..000000000
--- a/app/javascript/glitch/locales/en.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
-  "getting_started.open_source_notice": "Glitchsoc is free open source software forked from {Mastodon}. You can contribute or report issues on GitHub at {github}.",
-  "layout.auto": "Auto",
-  "layout.current_is": "Your current layout is:",
-  "layout.desktop": "Desktop",
-  "layout.mobile": "Mobile",
-  "navigation_bar.app_settings": "App settings",
-  "getting_started.onboarding": "Show me around",
-  "onboarding.page_one.federation": "{domain} is an 'instance' of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
-  "onboarding.page_one.welcome": "Welcome to {domain}!",
-  "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}, and is compatible with any Mastodon instance or app. Glitchsoc is entirely free and open-source. You can report bugs, request features, or contribute to the code on {github}.",
-  "settings.auto_collapse": "Automatic collapsing",
-  "settings.auto_collapse_all": "Everything",
-  "settings.auto_collapse_lengthy": "Lengthy toots",
-  "settings.auto_collapse_media": "Toots with media",
-  "settings.auto_collapse_notifications": "Notifications",
-  "settings.auto_collapse_reblogs": "Boosts",
-  "settings.auto_collapse_replies": "Replies",
-  "settings.close": "Close",
-  "settings.collapsed_statuses": "Collapsed toots",
-  "settings.enable_collapsed": "Enable collapsed toots",
-  "settings.general": "General",
-  "settings.image_backgrounds": "Image backgrounds",
-  "settings.image_backgrounds_media": "Preview collapsed toot media",
-  "settings.image_backgrounds_users": "Give collapsed toots an image background",
-  "settings.media": "Media",
-  "settings.media_letterbox": "Letterbox media",
-  "settings.media_fullwidth": "Full-width media previews",
-  "settings.preferences": "User preferences",
-  "settings.wide_view": "Wide view (Desktop mode only)",
-  "settings.navbar_under": "Navbar at the bottom (Mobile only)",
-  "status.collapse": "Collapse",
-  "status.uncollapse": "Uncollapse",
-
-  "home.column_settings.show_direct": "Show DMs",
-
-  "notification.markForDeletion": "Mark for deletion",
-  "notifications.clear": "Clear all my notifications",
-  "notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?",
-  "notifications.marked_clear": "Clear selected notifications",
-
-  "notification_purge.btn_all": "Select\nall",
-  "notification_purge.btn_none": "Select\nnone",
-  "notification_purge.btn_invert": "Invert\nselection",
-  "notification_purge.btn_apply": "Clear\nselected"
-}
diff --git a/app/javascript/glitch/locales/ja.json b/app/javascript/glitch/locales/ja.json
deleted file mode 100644
index 70091268f..000000000
--- a/app/javascript/glitch/locales/ja.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
-  "getting_started.open_source_notice": "Glitchsocは{Mastodon}によるフリーなオープンソースソフトウェアです。誰でもGitHub({github})から開発に參加したり、問題を報告したりできます。",
-  "layout.auto": "自動",
-  "layout.current_is": "あなたの現在のレイアウト:",
-  "layout.desktop": "デスクトップ",
-  "layout.mobile": "モバイル",
-  "navigation_bar.app_settings": "アプリ設定",
-  "getting_started.onboarding": "解説",
-  "onboarding.page_one.federation": "{domain}はMastodonのインスタンスです。Mastodonとは、独立したサーバが連携して作るソーシャルネットワークです。これらのサーバーをインスタンスと呼びます。",
-  "onboarding.page_one.welcome": "{domain}へようこそ!",
-  "onboarding.page_six.github": "{domain}はGlitchsocを使用しています。Glitchsocは{Mastodon}のフレンドリーな{fork}で、どんなMastodonアプリやインスタンスとも互換性があります。Glitchsocは完全に無料で、オープンソースです。{github}でバグ報告や機能要望あるいは貢獻をすることが可能です。",
-  "settings.auto_collapse": "自動折りたたみ",
-  "settings.auto_collapse_all": "すべて",
-  "settings.auto_collapse_lengthy": "長いトゥート",
-  "settings.auto_collapse_media": "メディア付きトゥート",
-  "settings.auto_collapse_notifications": "通知",
-  "settings.auto_collapse_reblogs": "ブースト",
-  "settings.auto_collapse_replies": "返信",
-  "settings.close": "閉じる",
-  "settings.collapsed_statuses": "トゥート",
-  "settings.enable_collapsed": "トゥート折りたたみを有効にする",
-  "settings.general": "一般",
-  "settings.image_backgrounds": "画像背景",
-  "settings.image_backgrounds_media": "折りたまれたメディア付きテゥートをプレビュー",
-  "settings.image_backgrounds_users": "折りたまれたトゥートの背景を変更する",
-  "settings.media": "メディア",
-  "settings.media_letterbox": "メディアをレターボックス式で表示",
-  "settings.media_fullwidth": "全幅メディアプリビュー",
-  "settings.preferences": "ユーザー設定",
-  "settings.wide_view": "ワイドビュー(デスクトップレイアウトのみ)",
-  "settings.navbar_under": "ナビを画面下部に移動させる(モバイルレイアウトのみ)",
-  "status.collapse": "折りたたむ",
-  "status.uncollapse": "折りたたみを解除",
-
-  "notification.markForDeletion": "選択",
-  "notifications.clear": "通知を全てクリアする",
-  "notifications.marked_clear_confirmation": "削除した全ての通知を完全に削除してもよろしいですか?",
-  "notifications.marked_clear": "選択した通知を削除する",
-
-  "notification_purge.btn_all": "すべて\n選択",
-  "notification_purge.btn_none": "選択\n解除",
-  "notification_purge.btn_invert": "選択を\n反転",
-  "notification_purge.btn_apply": "選択したものを\n削除"
-}
diff --git a/app/javascript/glitch/locales/pl.json b/app/javascript/glitch/locales/pl.json
deleted file mode 100644
index 1481b6a2a..000000000
--- a/app/javascript/glitch/locales/pl.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
-  "getting_started.open_source_notice": "Glitchsoc jest wolnym i otwartoźródłowym forkiem oprogramowania {Mastodon}. Możesz współtworzyć projekt lub zgłaszać błędy na GitHubie pod adresem {github}.",
-  "layout.auto": "Automatyczny",
-  "layout.current_is": "Twój obecny układ to:",
-  "layout.desktop": "Desktopowy",
-  "layout.mobile": "Mobilny",
-  "navigation_bar.app_settings": "Ustawienia aplikacji",
-  "getting_started.onboarding": "Rozejrzyj się",
-  "onboarding.page_one.federation": "{domain} jest 'instancją' Mastodona. Mastodon to sieć działających niezależnie serwerów tworzących jedną sieć społecznościową. Te serwery nazywane są instancjami.",
-  "onboarding.page_one.welcome": "Witamy na {domain}!",
-  "onboarding.page_six.github": "{domain} jest oparty na Glitchsoc. Glitchsoc jest {forkiem} {Mastodon}a kompatybilnym z każdym klientem i aplikacją Mastodona. Glitchsoc jest całkowicie wolnym i otwartoźródłowym oprogramowaniem. Możesz zgłaszać błędy i sugestie funkcji oraz współtworzyć projekt na {github}.",
-  "settings.auto_collapse": "Automatyczne zwijanie",
-  "settings.auto_collapse_all": "Wszystko",
-  "settings.auto_collapse_lengthy": "Długie wpisy",
-  "settings.auto_collapse_media": "Wpisy z zawartością multimedialną",
-  "settings.auto_collapse_notifications": "Powiadomienia",
-  "settings.auto_collapse_reblogs": "Podbicia",
-  "settings.auto_collapse_replies": "Odpowiedzi",
-  "settings.close": "Zamknij",
-  "settings.collapsed_statuses": "Zwijanie wpisów",
-  "settings.enable_collapsed": "Włącz zwijanie wpisów",
-  "settings.general": "Ogólne",
-  "settings.image_backgrounds": "Obrazy w tle",
-  "settings.image_backgrounds_media": "Wyświetlaj zawartość multimedialną zwiniętych wpisów",
-  "settings.image_backgrounds_users": "Nadaj tło zwiniętym wpisom",
-  "settings.media": "Zawartość multimedialna",
-  "settings.media_letterbox": "Letterbox media",
-  "settings.media_fullwidth": "Podgląd zawartości multimedialnej o pełnej szerokości",
-  "settings.preferences": "Preferencje użyytkownika",
-  "settings.wide_view": "Szeroki widok (tylko w trybie desktopowym)",
-  "settings.navbar_under": "Pasek nawigacji na dole (tylko w trybie mobilnym)",
-  "status.collapse": "Zwiń",
-  "status.uncollapse": "Rozwiń",
-
-  "notification.markForDeletion": "Oznacz do usunięcia",
-  "notifications.clear": "Wyczyść wszystkie powiadomienia",
-  "notifications.marked_clear_confirmation": "Czy na pewno chcesz bezpowrtonie usunąć wszystkie powiadomienia?",
-  "notifications.marked_clear": "Usuń zaznaczone powiadomienia",
-
-  "notification_purge.btn_all": "Zaznacz\nwszystkie",
-  "notification_purge.btn_none": "Odznacz\nwszystkie",
-  "notification_purge.btn_invert": "Odwróć\nzaznaczenie",
-  "notification_purge.btn_apply": "Usuń\nzaznaczone"
-}
diff --git a/app/javascript/images/screenshot.jpg b/app/javascript/images/screenshot.jpg
new file mode 100644
index 000000000..45b270fbb
--- /dev/null
+++ b/app/javascript/images/screenshot.jpg
Binary files differdiff --git a/app/javascript/mastodon/locales/locale-data/README.md b/app/javascript/locales/locale-data/README.md
index 83368fae7..83368fae7 100644
--- a/app/javascript/mastodon/locales/locale-data/README.md
+++ b/app/javascript/locales/locale-data/README.md
diff --git a/app/javascript/mastodon/locales/locale-data/oc.js b/app/javascript/locales/locale-data/oc.js
index c4b56350b..c4b56350b 100644
--- a/app/javascript/mastodon/locales/locale-data/oc.js
+++ b/app/javascript/locales/locale-data/oc.js
diff --git a/app/javascript/mastodon/actions/favourites.js b/app/javascript/mastodon/actions/favourites.js
index 09ce51fce..93094c526 100644
--- a/app/javascript/mastodon/actions/favourites.js
+++ b/app/javascript/mastodon/actions/favourites.js
@@ -10,6 +10,10 @@ export const FAVOURITED_STATUSES_EXPAND_FAIL    = 'FAVOURITED_STATUSES_EXPAND_FA
 
 export function fetchFavouritedStatuses() {
   return (dispatch, getState) => {
+    if (getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
+      return;
+    }
+
     dispatch(fetchFavouritedStatusesRequest());
 
     api(getState).get('/api/v1/favourites').then(response => {
@@ -46,7 +50,7 @@ export function expandFavouritedStatuses() {
   return (dispatch, getState) => {
     const url = getState().getIn(['status_lists', 'favourites', 'next'], null);
 
-    if (url === null) {
+    if (url === null || getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
       return;
     }
 
diff --git a/app/javascript/mastodon/actions/push_notifications.js b/app/javascript/mastodon/actions/push_notifications.js
index 55661d2b0..de06385f9 100644
--- a/app/javascript/mastodon/actions/push_notifications.js
+++ b/app/javascript/mastodon/actions/push_notifications.js
@@ -1,4 +1,5 @@
 import axios from 'axios';
+import { pushNotificationsSetting } from '../settings';
 
 export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
 export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
@@ -42,11 +43,15 @@ export function saveSettings() {
     const state = getState().get('push_notifications');
     const subscription = state.get('subscription');
     const alerts = state.get('alerts');
+    const data = { alerts };
 
     axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
-      data: {
-        alerts,
-      },
+      data,
+    }).then(() => {
+      const me = getState().getIn(['meta', 'me']);
+      if (me) {
+        pushNotificationsSetting.set(me, data);
+      }
     });
   };
 }
diff --git a/app/javascript/mastodon/components/avatar.js b/app/javascript/mastodon/components/avatar.js
index f7c484ee3..570505833 100644
--- a/app/javascript/mastodon/components/avatar.js
+++ b/app/javascript/mastodon/components/avatar.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import { autoPlayGif } from '../initial_state';
 
 export default class Avatar extends React.PureComponent {
 
@@ -8,12 +9,12 @@ export default class Avatar extends React.PureComponent {
     account: ImmutablePropTypes.map.isRequired,
     size: PropTypes.number.isRequired,
     style: PropTypes.object,
-    animate: PropTypes.bool,
     inline: PropTypes.bool,
+    animate: PropTypes.bool,
   };
 
   static defaultProps = {
-    animate: false,
+    animate: autoPlayGif,
     size: 20,
     inline: false,
   };
diff --git a/app/javascript/mastodon/components/avatar_overlay.js b/app/javascript/mastodon/components/avatar_overlay.js
index f5d67b34e..3ec1d7730 100644
--- a/app/javascript/mastodon/components/avatar_overlay.js
+++ b/app/javascript/mastodon/components/avatar_overlay.js
@@ -1,22 +1,29 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import { autoPlayGif } from '../initial_state';
 
 export default class AvatarOverlay extends React.PureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map.isRequired,
     friend: ImmutablePropTypes.map.isRequired,
+    animate: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    animate: autoPlayGif,
   };
 
   render() {
-    const { account, friend } = this.props;
+    const { account, friend, animate } = this.props;
 
     const baseStyle = {
-      backgroundImage: `url(${account.get('avatar_static')})`,
+      backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
     };
 
     const overlayStyle = {
-      backgroundImage: `url(${friend.get('avatar_static')})`,
+      backgroundImage: `url(${friend.get(animate ? 'avatar' : 'avatar_static')})`,
     };
 
     return (
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index 7890755f3..a876c5197 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -156,6 +156,8 @@ export default class ComposeForm extends ImmutablePureComponent {
 
     return (
       <div className='compose-form'>
+        <WarningContainer />
+
         <Collapsable isVisible={this.props.spoiler} fullHeight={50}>
           <div className='spoiler-input'>
             <label>
@@ -165,8 +167,6 @@ export default class ComposeForm extends ImmutablePureComponent {
           </div>
         </Collapsable>
 
-        <WarningContainer />
-
         <ReplyIndicatorContainer />
 
         <div className='compose-form__autosuggest-wrapper'>
@@ -199,11 +199,11 @@ export default class ComposeForm extends ImmutablePureComponent {
             <SensitiveButtonContainer />
             <SpoilerButtonContainer />
           </div>
+          <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
+        </div>
 
-          <div className='compose-form__publish'>
-            <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
-            <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
-          </div>
+        <div className='compose-form__publish'>
+          <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.js
index 7672440b4..d8cda96f3 100644
--- a/app/javascript/mastodon/features/compose/components/reply_indicator.js
+++ b/app/javascript/mastodon/features/compose/components/reply_indicator.js
@@ -6,6 +6,7 @@ import IconButton from '../../../components/icon_button';
 import DisplayName from '../../../components/display_name';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import { isRtl } from '../../../rtl';
 
 const messages = defineMessages({
   cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
@@ -42,7 +43,10 @@ export default class ReplyIndicator extends ImmutablePureComponent {
       return null;
     }
 
-    const content  = { __html: status.get('contentHtml') };
+    const content = { __html: status.get('contentHtml') };
+    const style   = {
+      direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
+    };
 
     return (
       <div className='reply-indicator'>
@@ -55,7 +59,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
           </a>
         </div>
 
-        <div className='reply-indicator__content' dangerouslySetInnerHTML={content} />
+        <div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} />
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js
index 6ab76492a..3a3d17710 100644
--- a/app/javascript/mastodon/features/compose/components/upload.js
+++ b/app/javascript/mastodon/features/compose/components/upload.js
@@ -62,7 +62,7 @@ export default class Upload extends ImmutablePureComponent {
   render () {
     const { intl, media } = this.props;
     const active          = this.state.hovered || this.state.focused;
-    const description     = this.state.dirtyDescription || media.get('description') || '';
+    const description     = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || '';
 
     return (
       <div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js
index 1e1f5873c..67b107bc8 100644
--- a/app/javascript/mastodon/features/favourited_statuses/index.js
+++ b/app/javascript/mastodon/features/favourited_statuses/index.js
@@ -9,6 +9,7 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 import StatusList from '../../components/status_list';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import { debounce } from 'lodash';
 
 const messages = defineMessages({
   heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
@@ -16,6 +17,7 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   statusIds: state.getIn(['status_lists', 'favourites', 'items']),
+  isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
   hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
 });
 
@@ -30,6 +32,7 @@ export default class Favourites extends ImmutablePureComponent {
     columnId: PropTypes.string,
     multiColumn: PropTypes.bool,
     hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -59,12 +62,12 @@ export default class Favourites extends ImmutablePureComponent {
     this.column = c;
   }
 
-  handleScrollToBottom = () => {
+  handleScrollToBottom = debounce(() => {
     this.props.dispatch(expandFavouritedStatuses());
-  }
+  }, 300, { leading: true })
 
   render () {
-    const { intl, statusIds, columnId, multiColumn, hasMore } = this.props;
+    const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
     const pinned = !!columnId;
 
     return (
@@ -85,6 +88,7 @@ export default class Favourites extends ImmutablePureComponent {
           statusIds={statusIds}
           scrollKey={`favourited_statuses-${columnId}`}
           hasMore={hasMore}
+          isLoading={isLoading}
           onScrollToBottom={this.handleScrollToBottom}
         />
       </Column>
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
index 1dcd4de14..ae136e48f 100644
--- a/app/javascript/mastodon/features/list_timeline/index.js
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -161,7 +161,7 @@ export default class ListTimeline extends React.PureComponent {
           scrollKey={`list_timeline-${columnId}`}
           timelineId={`list:${id}`}
           loadMore={this.handleLoadMore}
-          emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />}
+          emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
         />
       </Column>
     );
diff --git a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
index f15fbb2f4..f14be2aaf 100644
--- a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
+++ b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
@@ -8,6 +8,7 @@ import {
 } from '../../../actions/timelines';
 import Column from '../../../components/column';
 import ColumnHeader from '../../../components/column_header';
+import { connectHashtagStream } from '../../../actions/streaming';
 
 @connect()
 export default class HashtagTimeline extends React.PureComponent {
@@ -29,16 +30,13 @@ export default class HashtagTimeline extends React.PureComponent {
     const { dispatch, hashtag } = this.props;
 
     dispatch(refreshHashtagTimeline(hashtag));
-
-    this.polling = setInterval(() => {
-      dispatch(refreshHashtagTimeline(hashtag));
-    }, 10000);
+    this.disconnect = dispatch(connectHashtagStream(hashtag));
   }
 
   componentWillUnmount () {
-    if (typeof this.polling !== 'undefined') {
-      clearInterval(this.polling);
-      this.polling = null;
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
     }
   }
 
diff --git a/app/javascript/mastodon/features/standalone/public_timeline/index.js b/app/javascript/mastodon/features/standalone/public_timeline/index.js
index de4b5320a..5805d1a10 100644
--- a/app/javascript/mastodon/features/standalone/public_timeline/index.js
+++ b/app/javascript/mastodon/features/standalone/public_timeline/index.js
@@ -9,6 +9,7 @@ import {
 import Column from '../../../components/column';
 import ColumnHeader from '../../../components/column_header';
 import { defineMessages, injectIntl } from 'react-intl';
+import { connectPublicStream } from '../../../actions/streaming';
 
 const messages = defineMessages({
   title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
@@ -35,16 +36,13 @@ export default class PublicTimeline extends React.PureComponent {
     const { dispatch } = this.props;
 
     dispatch(refreshPublicTimeline());
-
-    this.polling = setInterval(() => {
-      dispatch(refreshPublicTimeline());
-    }, 3000);
+    this.disconnect = dispatch(connectPublicStream());
   }
 
   componentWillUnmount () {
-    if (typeof this.polling !== 'undefined') {
-      clearInterval(this.polling);
-      this.polling = null;
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
     }
   }
 
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js
index d3e322c36..2f6a7831e 100644
--- a/app/javascript/mastodon/features/status/components/card.js
+++ b/app/javascript/mastodon/features/status/components/card.js
@@ -43,7 +43,7 @@ export default class Card extends React.PureComponent {
       Immutable.fromJS([
         {
           type: 'image',
-          url: card.get('url'),
+          url: card.get('embed_url'),
           description: card.get('title'),
           meta: {
             original: {
@@ -59,6 +59,8 @@ export default class Card extends React.PureComponent {
 
   renderLink () {
     const { card, maxDescription } = this.props;
+    const { width }  = this.state;
+    const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width);
 
     let image    = '';
     let provider = card.get('provider_name');
@@ -75,17 +77,15 @@ export default class Card extends React.PureComponent {
       provider = decodeIDNA(getHostname(card.get('url')));
     }
 
-    const className = classnames('status-card', {
-      'horizontal': card.get('width') > card.get('height'),
-    });
+    const className = classnames('status-card', { horizontal });
 
     return (
-      <a href={card.get('url')} className={className} target='_blank' rel='noopener'>
+      <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}>
         {image}
 
         <div className='status-card__content'>
           <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
-          <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>
+          {!horizontal && <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>}
           <span className='status-card__host'>{provider}</span>
         </div>
       </a>
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 93ed9e605..f00b74dfd 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -53,7 +53,10 @@ export default class ColumnsArea extends ImmutablePureComponent {
     if (!this.props.singleColumn) {
       this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
     }
-    this.lastIndex = getIndex(this.context.router.history.location.pathname);
+
+    this.lastIndex   = getIndex(this.context.router.history.location.pathname);
+    this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
+
     this.setState({ shouldAnimate: true });
   }
 
@@ -79,7 +82,8 @@ export default class ColumnsArea extends ImmutablePureComponent {
 
   handleChildrenContentChange() {
     if (!this.props.singleColumn) {
-      this._interruptScrollAnimation = scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
+      const modifier = this.isRtlLayout ? -1 : 1;
+      this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
     }
   }
 
diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js
index 1437deeb0..6a883759f 100644
--- a/app/javascript/mastodon/features/ui/components/video_modal.js
+++ b/app/javascript/mastodon/features/ui/components/video_modal.js
@@ -23,6 +23,7 @@ export default class VideoModal extends ImmutablePureComponent {
             src={media.get('url')}
             startTime={time}
             onCloseVideo={onClose}
+            detailed
             description={media.get('description')}
           />
         </div>
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 003bf23a8..0ee8bb6c8 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -17,6 +17,18 @@ const messages = defineMessages({
   exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
 });
 
+const formatTime = secondsNum => {
+  let hours   = Math.floor(secondsNum / 3600);
+  let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
+  let seconds = secondsNum - (hours * 3600) - (minutes * 60);
+
+  if (hours   < 10) hours   = '0' + hours;
+  if (minutes < 10) minutes = '0' + minutes;
+  if (seconds < 10) seconds = '0' + seconds;
+
+  return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`;
+};
+
 const findElementPosition = el => {
   let box;
 
@@ -83,11 +95,13 @@ export default class Video extends React.PureComponent {
     startTime: PropTypes.number,
     onOpenVideo: PropTypes.func,
     onCloseVideo: PropTypes.func,
+    detailed: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
   state = {
-    progress: 0,
+    currentTime: 0,
+    duration: 0,
     paused: true,
     dragging: false,
     fullscreen: false,
@@ -117,7 +131,10 @@ export default class Video extends React.PureComponent {
   }
 
   handleTimeUpdate = () => {
-    this.setState({ progress: 100 * (this.video.currentTime / this.video.duration) });
+    this.setState({
+      currentTime: Math.floor(this.video.currentTime),
+      duration: Math.floor(this.video.duration),
+    });
   }
 
   handleMouseDown = e => {
@@ -143,8 +160,10 @@ export default class Video extends React.PureComponent {
 
   handleMouseMove = throttle(e => {
     const { x } = getPointerPosition(this.seek, e);
-    this.video.currentTime = this.video.duration * x;
-    this.setState({ progress: x * 100 });
+    const currentTime = Math.floor(this.video.duration * x);
+
+    this.video.currentTime = currentTime;
+    this.setState({ currentTime });
   }, 60);
 
   togglePlay = () => {
@@ -226,11 +245,12 @@ export default class Video extends React.PureComponent {
   }
 
   render () {
-    const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt } = this.props;
-    const { progress, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
+    const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed } = this.props;
+    const { currentTime, duration, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
+    const progress = (currentTime / duration) * 100;
 
     return (
-      <div className={classNames('video-player', { inactive: !revealed, inline: width && height && !fullscreen, fullscreen })} style={{ width, height }} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
+      <div className={classNames('video-player', { inactive: !revealed, detailed, inline: width && height && !fullscreen, fullscreen })} style={{ width, height }} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
         <video
           ref={this.setVideoRef}
           src={src}
@@ -267,16 +287,27 @@ export default class Video extends React.PureComponent {
             />
           </div>
 
-          <div className='video-player__buttons left'>
-            <button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button>
-            <button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button>
-            {!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>}
-          </div>
-
-          <div className='video-player__buttons right'>
-            {(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>}
-            {onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-times' /></button>}
-            <button aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button>
+          <div className='video-player__buttons-bar'>
+            <div className='video-player__buttons left'>
+              <button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button>
+              <button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button>
+
+              {!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>}
+
+              {(detailed || fullscreen) &&
+                <span>
+                  <span className='video-player__time-current'>{formatTime(currentTime)}</span>
+                  <span className='video-player__time-sep'>/</span>
+                  <span className='video-player__time-total'>{formatTime(duration)}</span>
+                </span>
+              }
+            </div>
+
+            <div className='video-player__buttons right'>
+              {(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>}
+              {onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-compress' /></button>}
+              <button aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button>
+            </div>
           </div>
         </div>
       </div>
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index a984b38d2..d699a69df 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -36,9 +36,9 @@
   "column.favourites": "المفضلة",
   "column.follow_requests": "طلبات المتابعة",
   "column.home": "الرئيسية",
-  "column.lists": "Lists",
+  "column.lists": "القوائم",
   "column.mutes": "الحسابات المكتومة",
-  "column.notifications": "الإشعارات",
+  "column.notifications": "الإخطارات",
   "column.pins": "التبويقات المثبتة",
   "column.public": "الخيط العام الموحد",
   "column_back_button.label": "العودة",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "حذف",
   "confirmations.delete.message": "هل أنت متأكد أنك تريد حذف هذا المنشور ؟",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "هل تود حقا حذف هذه القائمة ؟",
   "confirmations.domain_block.confirm": "إخفاء إسم النطاق كاملا",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "أكتم",
@@ -109,32 +109,32 @@
   "home.settings": "إعدادات العمود",
   "keyboard_shortcuts.back": "للعودة",
   "keyboard_shortcuts.boost": "للترقية",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.column": "للتركيز على منشور على أحد الأعمدة",
+  "keyboard_shortcuts.compose": "للتركيز على نافذة تحرير النصوص",
   "keyboard_shortcuts.description": "Description",
   "keyboard_shortcuts.down": "للإنتقال إلى أسفل القائمة",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "للإضافة إلى المفضلة",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.hotkey": "مفتاح الإختصار",
+  "keyboard_shortcuts.legend": "لعرض هذا المفتاح",
   "keyboard_shortcuts.mention": "لذِكر الناشر",
   "keyboard_shortcuts.reply": "للردّ",
-  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.search": "للتركيز على البحث",
   "keyboard_shortcuts.toot": "لتحرير تبويق جديد",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة",
   "lightbox.close": "إغلاق",
   "lightbox.next": "التالي",
   "lightbox.previous": "العودة",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "أضف إلى القائمة",
+  "lists.account.remove": "إحذف من القائمة",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
-  "lists.subheading": "Your lists",
+  "lists.edit": "تعديل القائمة",
+  "lists.new.create": "إنشاء قائمة",
+  "lists.new.title_placeholder": "عنوان القائمة الجديدة",
+  "lists.search": "إبحث في قائمة الحسابات التي تُتابِعها",
+  "lists.subheading": "قوائمك",
   "loading_indicator.label": "تحميل ...",
   "media_gallery.toggle_visible": "عرض / إخفاء",
   "missing_indicator.label": "تعذر العثور عليه",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "طلبات المتابعة",
   "navigation_bar.info": "معلومات إضافية",
   "navigation_bar.keyboard_shortcuts": "إختصارات لوحة المفاتيح",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "القوائم",
   "navigation_bar.logout": "خروج",
   "navigation_bar.mutes": "الحسابات المكتومة",
   "navigation_bar.pins": "التبويقات المثبتة",
@@ -209,7 +209,7 @@
   "search_popout.search_format": "نمط البحث المتقدم",
   "search_popout.tips.hashtag": "وسم",
   "search_popout.tips.status": "حالة",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.text": "جملة قصيرة تُمكّنُك من عرض أسماء و حسابات و كلمات رمزية",
   "search_popout.tips.user": "مستخدِم",
   "search_results.total": "{count, number} {count, plural, one {result} و {results}}",
   "standalone.public_title": "نظرة على ...",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index d20120b11..1c04b3bfa 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Зареждане...",
   "media_gallery.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index bfa931fc0..62d85a5e1 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -36,7 +36,7 @@
   "column.favourites": "Favorits",
   "column.follow_requests": "Peticions per seguir-te",
   "column.home": "Inici",
-  "column.lists": "Lists",
+  "column.lists": "Llistes",
   "column.mutes": "Usuaris silenciats",
   "column.notifications": "Notificacions",
   "column.pins": "Toot fixat",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Esborrar",
   "confirmations.delete.message": "Estàs segur que vols esborrar aquest estat?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Estàs segur que vols esborrar permanenment aquesta llista?",
   "confirmations.domain_block.confirm": "Amagar tot el domini",
   "confirmations.domain_block.message": "Estàs realment, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar és suficient i preferible.",
   "confirmations.mute.confirm": "Silenciar",
@@ -127,14 +127,14 @@
   "lightbox.close": "Tancar",
   "lightbox.next": "Següent",
   "lightbox.previous": "Anterior",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Afegir a la llista",
+  "lists.account.remove": "Treure de la llista",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Editar llista",
+  "lists.new.create": "Afegir llista",
+  "lists.new.title_placeholder": "Nou títol de llista",
+  "lists.search": "Cercar entre les persones que segueixes",
+  "lists.subheading": "Les teves llistes",
   "loading_indicator.label": "Carregant...",
   "media_gallery.toggle_visible": "Alternar visibilitat",
   "missing_indicator.label": "No trobat",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Sol·licituds de seguiment",
   "navigation_bar.info": "Informació addicional",
   "navigation_bar.keyboard_shortcuts": "Dreceres de teclat",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Llistes",
   "navigation_bar.logout": "Tancar sessió",
   "navigation_bar.mutes": "Usuaris silenciats",
   "navigation_bar.pins": "Toots fixats",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index b6d9e27a7..6354f18b6 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Wird geladen …",
   "media_gallery.toggle_visible": "Sichtbarkeit umschalten",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index bb82cf5f5..65e20c17a 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -1296,6 +1296,19 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Favourite",
+        "id": "status.favourite"
+      },
+      {
+        "defaultMessage": "You can press {combo} to skip this next time",
+        "id": "favourite_modal.combo"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/ui/components/favourite_modal.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Network error",
         "id": "bundle_column_error.title"
       },
@@ -1601,4 +1614,4 @@
     ],
     "path": "app/javascript/mastodon/features/video/index.json"
   }
-]
\ No newline at end of file
+]
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 538124904..3fc4a8c96 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -33,6 +33,7 @@
   "bundle_modal_error.retry": "Try again",
   "column.blocks": "Blocked users",
   "column.community": "Local timeline",
+  "column.direct": "Direct messages",
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Home",
@@ -88,10 +89,11 @@
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
+  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.public_timeline": "the public timeline",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
   "follow_request.authorize": "Authorize",
@@ -133,7 +135,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Loading...",
   "media_gallery.toggle_visible": "Toggle visibility",
@@ -141,6 +143,7 @@
   "mute_modal.hide_notifications": "Hide notifications from this user?",
   "navigation_bar.blocks": "Blocked users",
   "navigation_bar.community_timeline": "Local timeline",
+  "navigation_bar.direct": "Direct messages",
   "navigation_bar.edit_profile": "Edit profile",
   "navigation_bar.favourites": "Favourites",
   "navigation_bar.follow_requests": "Follow requests",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 619c7320a..9e66c379f 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Ŝarganta…",
   "media_gallery.toggle_visible": "Baskuli videblecon",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 411615744..6122a79ab 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Cargando…",
   "media_gallery.toggle_visible": "Cambiar visibilidad",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index aa5c21feb..75057a7dd 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "بارگیری...",
   "media_gallery.toggle_visible": "تغییر پیدایی",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index db9319e2e..4ddc1cca7 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Ladataan...",
   "media_gallery.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 9b9469bc2..ecfff87c8 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -63,8 +63,8 @@
   "confirmations.block.message": "Confirmez-vous le blocage de {name} ?",
   "confirmations.delete.confirm": "Supprimer",
   "confirmations.delete.message": "Confirmez-vous la suppression de ce pouet ?",
-  "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.confirm": "Supprimer",
+  "confirmations.delete_list.message": "Êtes-vous sûr de vouloir supprimer définitivement cette liste ?",
   "confirmations.domain_block.confirm": "Masquer le domaine entier",
   "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables.",
   "confirmations.mute.confirm": "Masquer",
@@ -91,7 +91,7 @@
   "empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag",
   "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.",
   "empty_column.home.public_timeline": "le fil public",
-  "empty_column.list": "Il n'y a rien dans cette liste pour l'instant.",
+  "empty_column.list": "Il n'y a rien dans cette liste pour l'instant. Dès que des personnes de cette liste publierons de nouveaux statuts, ils apparaîtront ici.",
   "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateur⋅ice⋅s pour débuter la conversation.",
   "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.",
   "follow_request.authorize": "Accepter",
@@ -113,28 +113,28 @@
   "keyboard_shortcuts.compose": "pour centrer la zone de redaction",
   "keyboard_shortcuts.description": "Description",
   "keyboard_shortcuts.down": "descendre dans la liste",
-  "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.enter": "pour ouvrir le statut",
+  "keyboard_shortcuts.favourite": "vers les favoris",
+  "keyboard_shortcuts.heading": "Raccourcis clavier",
+  "keyboard_shortcuts.hotkey": "Raccourci",
+  "keyboard_shortcuts.legend": "pour afficher cette légende",
+  "keyboard_shortcuts.mention": "pour mentionner l'auteur",
+  "keyboard_shortcuts.reply": "pour répondre",
   "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.toot": "pour démarrer un tout nouveau pouet",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Fermer",
   "lightbox.next": "Suivant",
   "lightbox.previous": "Précédent",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
-  "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
-  "lists.subheading": "Your lists",
+  "lists.account.add": "Ajouter à la liste",
+  "lists.account.remove": "Supprimer de la liste",
+  "lists.delete": "Effacer la liste",
+  "lists.edit": "Éditer la liste",
+  "lists.new.create": "Ajouter une liste",
+  "lists.new.title_placeholder": "Titre de la nouvelle liste",
+  "lists.search": "Rechercher parmi les gens que vous suivez",
+  "lists.subheading": "Vos listes",
   "loading_indicator.label": "Chargement…",
   "media_gallery.toggle_visible": "Modifier la visibilité",
   "missing_indicator.label": "Non trouvé",
@@ -145,8 +145,8 @@
   "navigation_bar.favourites": "Favoris",
   "navigation_bar.follow_requests": "Demandes de suivi",
   "navigation_bar.info": "Plus d’informations",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.keyboard_shortcuts": "Raccourcis clavier",
+  "navigation_bar.lists": "Listes",
   "navigation_bar.logout": "Déconnexion",
   "navigation_bar.mutes": "Comptes masqués",
   "navigation_bar.pins": "Pouets épinglés",
@@ -241,7 +241,7 @@
   "tabs_bar.home": "Accueil",
   "tabs_bar.local_timeline": "Fil public local",
   "tabs_bar.notifications": "Notifications",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.",
   "upload_area.title": "Glissez et déposez pour envoyer",
   "upload_button.label": "Joindre un média",
   "upload_form.description": "Décrire pour les malvoyants",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
new file mode 100644
index 000000000..6398daa11
--- /dev/null
+++ b/app/javascript/mastodon/locales/gl.json
@@ -0,0 +1,259 @@
+{
+  "account.block": "Bloquear @{name}",
+  "account.block_domain": "Ocultar calquer contido de {domain}",
+  "account.disclaimer_full": "A información inferior podería mostrar un perfil incompleto da usuaria.",
+  "account.edit_profile": "Editar perfil",
+  "account.follow": "Seguir",
+  "account.followers": "Seguidoras",
+  "account.follows": "Seguindo",
+  "account.follows_you": "Séguena",
+  "account.hide_reblogs": "Ocultar repeticións de @{name}",
+  "account.media": "Medios",
+  "account.mention": "Mencionar @{name}",
+  "account.moved_to": "{name} marchou a:",
+  "account.mute": "Acalar @{name}",
+  "account.mute_notifications": "Acalar as notificacións de @{name}",
+  "account.posts": "Publicacións",
+  "account.report": "Informar sobre @{name}",
+  "account.requested": "Agardando aceptación. Pulse para cancelar a solicitude de seguimento",
+  "account.share": "Compartir o perfil de @{name}",
+  "account.show_reblogs": "Mostrar repeticións de @{name}",
+  "account.unblock": "Desbloquear @{name}",
+  "account.unblock_domain": "Non ocultar {domain}",
+  "account.unfollow": "Non seguir",
+  "account.unmute": "Non acalar @{name}",
+  "account.unmute_notifications": "Desbloquear as notificacións de @{name}",
+  "account.view_full_profile": "Ver o perfil completo",
+  "boost_modal.combo": "Pulse {combo} para saltar esto a próxima vez",
+  "bundle_column_error.body": "Houbo un fallo mentras se cargaba este compoñente.",
+  "bundle_column_error.retry": "Inténteo de novo",
+  "bundle_column_error.title": "Fallo na rede",
+  "bundle_modal_error.close": "Pechar",
+  "bundle_modal_error.message": "Algo fallou mentras se cargaba este compoñente.",
+  "bundle_modal_error.retry": "Inténteo de novo",
+  "column.blocks": "Usuarias bloqueadas",
+  "column.community": "Liña temporal local",
+  "column.favourites": "Favoritas",
+  "column.follow_requests": "Peticións de seguimento",
+  "column.home": "Inicio",
+  "column.lists": "Listas",
+  "column.mutes": "Usuarias acaladas",
+  "column.notifications": "Notificacións",
+  "column.pins": "Mensaxes fixadas",
+  "column.public": "Liña temporal federada",
+  "column_back_button.label": "Atrás",
+  "column_header.hide_settings": "Agochar axustes",
+  "column_header.moveLeft_settings": "Mover a columna hacia a esquerda",
+  "column_header.moveRight_settings": "Mover a columna hacia a dereita",
+  "column_header.pin": "Fixar",
+  "column_header.show_settings": "Mostras axustes",
+  "column_header.unpin": "Soltar",
+  "column_subheading.navigation": "Navegación",
+  "column_subheading.settings": "Axustes",
+  "compose_form.lock_disclaimer": "A súa conta non está {locked}. Calquera pode seguila para ver as súas mensaxes só-para-seguidoras.",
+  "compose_form.lock_disclaimer.lock": "bloqueado",
+  "compose_form.placeholder": "A qué andas?",
+  "compose_form.publish": "Toot",
+  "compose_form.publish_loud": "{publish}!",
+  "compose_form.sensitive": "Marcar medios como sensibles",
+  "compose_form.spoiler": "Agochar texto detrás de un aviso",
+  "compose_form.spoiler_placeholder": "Escriba o aviso aquí",
+  "confirmation_modal.cancel": "Cancelar",
+  "confirmations.block.confirm": "Bloquear",
+  "confirmations.block.message": "Está segura de querer bloquear a {name}?",
+  "confirmations.delete.confirm": "Borrar",
+  "confirmations.delete.message": "Está segura de que quere eliminar este estado?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Estás seguro de que queres eliminar permanentemente esta lista?",
+  "confirmations.domain_block.confirm": "Agochar un dominio completo",
+  "confirmations.domain_block.message": "Realmente está segura de que quere bloquear por completo o dominio {domain}? Normalmente é suficiente, e preferible, bloquear de xeito selectivo varios elementos.",
+  "confirmations.mute.confirm": "Acalar",
+  "confirmations.mute.message": "Está segura de que quere acalar a {name}?",
+  "confirmations.unfollow.confirm": "Deixar de seguir",
+  "confirmations.unfollow.message": "Quere deixar de seguir a {name}?",
+  "embed.instructions": "Copie o código inferior para incrustar no seu sitio web este estado.",
+  "embed.preview": "Así será mostrado:",
+  "emoji_button.activity": "Actividade",
+  "emoji_button.custom": "Personalizado",
+  "emoji_button.flags": "Marcas",
+  "emoji_button.food": "Comida e Bebida",
+  "emoji_button.label": "Insertar emoji",
+  "emoji_button.nature": "Natureza",
+  "emoji_button.not_found": "Sen emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Obxetos",
+  "emoji_button.people": "Xente",
+  "emoji_button.recent": "Utilizadas con frecuencia",
+  "emoji_button.search": "Buscar...",
+  "emoji_button.search_results": "Resultados da busca",
+  "emoji_button.symbols": "Símbolos",
+  "emoji_button.travel": "Viaxes e Lugares",
+  "empty_column.community": "A liña temporal local está baldeira. Escriba algo de xeito público para que rule!",
+  "empty_column.hashtag": "Aínda non hai nada con esta etiqueta.",
+  "empty_column.home": "A súa liña temporal de inicio está baldeira! Visite {public} ou utilice a busca para atopar outras usuarias.",
+  "empty_column.home.public_timeline": "a liña temporal pública",
+  "empty_column.list": "Aínda non hai nada en esta lista.",
+  "empty_column.notifications": "Aínda non ten notificacións. Interactúe con outras para iniciar unha conversa.",
+  "empty_column.public": "Nada por aquí! Escriba algo de xeito público, ou siga manualmente usuarias de outras instancias para ir enchéndoa",
+  "follow_request.authorize": "Autorizar",
+  "follow_request.reject": "Rexeitar",
+  "getting_started.appsshort": "Aplicacións",
+  "getting_started.faq": "PMF",
+  "getting_started.heading": "Comezando",
+  "getting_started.open_source_notice": "Mastodon é software de código aberto. Pode contribuír ou informar de fallos en GitHub en {github}.",
+  "getting_started.userguide": "Guía de usuaria",
+  "home.column_settings.advanced": "Avanzado",
+  "home.column_settings.basic": "Básico",
+  "home.column_settings.filter_regex": "Filtrar expresións regulares",
+  "home.column_settings.show_reblogs": "Mostrar repeticións",
+  "home.column_settings.show_replies": "Mostrar respostas",
+  "home.settings": "Axustes da columna",
+  "keyboard_shortcuts.back": "voltar atrás",
+  "keyboard_shortcuts.boost": "repetir",
+  "keyboard_shortcuts.column": "destacar un estado en unha das columnas",
+  "keyboard_shortcuts.compose": "Foco no área de escritura",
+  "keyboard_shortcuts.description": "Descrición",
+  "keyboard_shortcuts.down": "ir hacia abaixo na lista",
+  "keyboard_shortcuts.enter": "abrir estado",
+  "keyboard_shortcuts.favourite": "marcar como favorito",
+  "keyboard_shortcuts.heading": "Atallos do teclado",
+  "keyboard_shortcuts.hotkey": "Tecla de acceso directo",
+  "keyboard_shortcuts.legend": "para mostrar esta lenda",
+  "keyboard_shortcuts.mention": "para mencionar o autor",
+  "keyboard_shortcuts.reply": "para responder",
+  "keyboard_shortcuts.search": "para centrar a busca",
+  "keyboard_shortcuts.toot": "escribir un toot novo",
+  "keyboard_shortcuts.unfocus": "quitar o foco do área de escritura/busca",
+  "keyboard_shortcuts.up": "ir hacia arriba na lista",
+  "lightbox.close": "Fechar",
+  "lightbox.next": "Seguinte",
+  "lightbox.previous": "Anterior",
+  "lists.account.add": "Engadir á lista",
+  "lists.account.remove": "Eliminar da lista",
+  "lists.delete": "Delete list",
+  "lists.edit": "Editar lista",
+  "lists.new.create": "Engadir lista",
+  "lists.new.title_placeholder": "Novo título da lista",
+  "lists.search": "Procurar entre a xente que segues",
+  "lists.subheading": "As túas listas",
+  "loading_indicator.label": "Cargando...",
+  "media_gallery.toggle_visible": "Dar visibilidade",
+  "missing_indicator.label": "Non atopado",
+  "mute_modal.hide_notifications": "Esconder notificacións deste usuario?",
+  "navigation_bar.blocks": "Usuarios bloqueados",
+  "navigation_bar.community_timeline": "Liña temporal local",
+  "navigation_bar.edit_profile": "Editar perfil",
+  "navigation_bar.favourites": "Favoritas",
+  "navigation_bar.follow_requests": "Peticións de seguimento",
+  "navigation_bar.info": "Sobre esta instancia",
+  "navigation_bar.keyboard_shortcuts": "Atallos do teclado",
+  "navigation_bar.lists": "Listas",
+  "navigation_bar.logout": "Sair",
+  "navigation_bar.mutes": "Usuarias acaladas",
+  "navigation_bar.pins": "Mensaxes fixadas",
+  "navigation_bar.preferences": "Preferencias",
+  "navigation_bar.public_timeline": "Liña temporal federada",
+  "notification.favourite": "{name} marcou como favorito o seu estado",
+  "notification.follow": "{name} está a seguila",
+  "notification.mention": "{name} mencionoute",
+  "notification.reblog": "{name} promocionou o seu estado",
+  "notifications.clear": "Limpar notificacións",
+  "notifications.clear_confirmation": "Estás seguro de que queres limpar permanentemente todas as túas notificacións?",
+  "notifications.column_settings.alert": "Notificacións de escritorio",
+  "notifications.column_settings.favourite": "Favoritas:",
+  "notifications.column_settings.follow": "Novos seguidores:",
+  "notifications.column_settings.mention": "Mencións:",
+  "notifications.column_settings.push": "Enviar notificacións",
+  "notifications.column_settings.push_meta": "Este aparello",
+  "notifications.column_settings.reblog": "Promocións:",
+  "notifications.column_settings.show": "Mostrar en columna",
+  "notifications.column_settings.sound": "Reproducir son",
+  "onboarding.done": "Feito",
+  "onboarding.next": "Seguinte",
+  "onboarding.page_five.public_timelines": "A liña de tempo local mostra as publicacións públicas de todos en {domain}. A liña de tempo federada mostra as publicacións públicas de todos os que as persoas en {domain} seguen. Estas son as Liñas de tempo públicas, unha boa forma de descubrir novas persoas.",
+  "onboarding.page_four.home": "A liña de tempo local mostra as publicacións das persoas que segues.",
+  "onboarding.page_four.notifications": "A columna de notificacións mostra cando alguén interactúa contigo.",
+  "onboarding.page_one.federation": "Mastodon é unha rede de servidores independentes que se unen para facer unha rede social máis grande. Chamamos instancias a estes servidores.",
+  "onboarding.page_one.handle": "Estás en {domain}, polo que o teu nome de usuario completo é {handle}",
+  "onboarding.page_one.welcome": "Benvido a Mastodon!",
+  "onboarding.page_six.admin": "O administrador da túa instancia é {admin}.",
+  "onboarding.page_six.almost_done": "Case feito...",
+  "onboarding.page_six.appetoot": "Que tootes ben!",
+  "onboarding.page_six.apps_available": "Hai {apps} dispoñíbeis para iOS, Android e outras plataformas.",
+  "onboarding.page_six.github": "Mastodon é un software gratuito e de código aberto. Pode informar de erros, solicitar novas funcionalidades ou contribuír ao código en {github}.",
+  "onboarding.page_six.guidelines": "directrices da comunidade",
+  "onboarding.page_six.read_guidelines": "Por favor, le as {guidelines} do {domain}!",
+  "onboarding.page_six.various_app": "aplicacións móbiles",
+  "onboarding.page_three.profile": "Edita o teu perfil para cambiar o teu avatar, bio e nome. Alí, tamén atoparás outras preferencias.",
+  "onboarding.page_three.search": "Utilice a barra de busca para atopar xente e descubrir etiquetas, como {illustration} e {introductions}. Para atopar unha usuaria que non está en esta instancia utilice o seu enderezo completo.",
+  "onboarding.page_two.compose": "Escriba mensaxes desde a columna de composición. Pode subir imaxes, mudar as opcións de intimidade e engadir avisos sobre o contido coas iconas inferiores.",
+  "onboarding.skip": "Saltar",
+  "privacy.change": "Axustar a intimidade do estado",
+  "privacy.direct.long": "Enviar exclusivamente as usuarias mencionadas",
+  "privacy.direct.short": "Directa",
+  "privacy.private.long": "Enviar só as seguidoras",
+  "privacy.private.short": "Só-seguidoras",
+  "privacy.public.long": "Publicar na liña temporal pública",
+  "privacy.public.short": "Pública",
+  "privacy.unlisted.long": "Non publicar en liñas temporais públicas",
+  "privacy.unlisted.short": "Non listada",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "agora",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
+  "reply_indicator.cancel": "Cancelar",
+  "report.placeholder": "Comentarios adicionais",
+  "report.submit": "Enviar",
+  "report.target": "Informar {target}",
+  "search.placeholder": "Buscar",
+  "search_popout.search_format": "Formato de busca avanzada",
+  "search_popout.tips.hashtag": "etiqueta",
+  "search_popout.tips.status": "estado",
+  "search_popout.tips.text": "Texto simple devolve coincidencias con nomes públicos, nomes de usuaria e etiquetas",
+  "search_popout.tips.user": "usuaria",
+  "search_results.total": "{count, number} {count,plural,one {result} outros {results}}",
+  "standalone.public_title": "Ollada dentro...",
+  "status.cannot_reblog": "Esta mensaxe non pode ser promocionada",
+  "status.delete": "Eliminar",
+  "status.embed": "Incrustar",
+  "status.favourite": "Favorita",
+  "status.load_more": "Cargar máis",
+  "status.media_hidden": "Medios ocultos",
+  "status.mention": "Mencionar @{name}",
+  "status.more": "Máis",
+  "status.mute_conversation": "Acalar conversa",
+  "status.open": "Expandir este estado",
+  "status.pin": "Fixar no perfil",
+  "status.reblog": "Promocionar",
+  "status.reblogged_by": "{name} promocionado",
+  "status.reply": "Resposta",
+  "status.replyAll": "Resposta a conversa",
+  "status.report": "Informar @{name}",
+  "status.sensitive_toggle": "Pulse para ver",
+  "status.sensitive_warning": "Contido sensible",
+  "status.share": "Compartir",
+  "status.show_less": "Mostrar menos",
+  "status.show_more": "Mostrar máis",
+  "status.unmute_conversation": "Non acalar a conversa",
+  "status.unpin": "Despegar do perfil",
+  "tabs_bar.compose": "Compoñer",
+  "tabs_bar.federated_timeline": "Federado",
+  "tabs_bar.home": "Inicio",
+  "tabs_bar.local_timeline": "Local",
+  "tabs_bar.notifications": "Notificacións",
+  "ui.beforeunload": "O borrador perderase se sae de Mastodon.",
+  "upload_area.title": "Arrastre e solte para subir",
+  "upload_button.label": "Engadir medios",
+  "upload_form.description": "Describa para deficientes visuais",
+  "upload_form.undo": "Desfacer",
+  "upload_progress.label": "Subindo...",
+  "video.close": "Pechar video",
+  "video.exit_fullscreen": "Saír da pantalla completa",
+  "video.expand": "Expandir vídeo",
+  "video.fullscreen": "Pantalla completa",
+  "video.hide": "Agochar vídeo",
+  "video.mute": "Acalar son",
+  "video.pause": "Pausar",
+  "video.play": "Reproducir",
+  "video.unmute": "Permitir son"
+}
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index ec1e30dd5..5444c8e34 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "טוען...",
   "media_gallery.toggle_visible": "נראה\\בלתי נראה",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index c21482670..f70c66223 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Učitavam...",
   "media_gallery.toggle_visible": "Preklopi vidljivost",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 71dd810b6..7cb816fe9 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Betöltés...",
   "media_gallery.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 744423e78..429b77182 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Tunggu sebentar...",
   "media_gallery.toggle_visible": "Tampil/Sembunyikan",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index b1523e626..3e5c8edb9 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Kargante...",
   "media_gallery.toggle_visible": "Chanjar videbleso",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 9a2d320fd..e2ad1632a 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Carico...",
   "media_gallery.toggle_visible": "Imposta visibilità",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index e015c41c2..68abd906f 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -24,7 +24,7 @@
   "account.unmute": "ミュート解除",
   "account.unmute_notifications": "@{name}さんからの通知を受け取らない",
   "account.view_full_profile": "全ての情報を見る",
-  "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
+  "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます",
   "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
   "bundle_column_error.retry": "再試行",
   "bundle_column_error.title": "ネットワークエラー",
@@ -33,6 +33,7 @@
   "bundle_modal_error.retry": "再試行",
   "column.blocks": "ブロックしたユーザー",
   "column.community": "ローカルタイムライン",
+  "column.direct": "ダイレクトメッセージ",
   "column.favourites": "お気に入り",
   "column.follow_requests": "フォローリクエスト",
   "column.home": "ホーム",
@@ -88,12 +89,13 @@
   "emoji_button.symbols": "記号",
   "emoji_button.travel": "旅行と場所",
   "empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
+  "empty_column.direct": "あなたはまだダイレクトメッセージを受け取っていません。あなたが送ったり受け取ったりすると、ここに表示されます。",
   "empty_column.hashtag": "このハッシュタグはまだ使われていません。",
   "empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
   "empty_column.home.public_timeline": "連合タイムライン",
   "empty_column.list": "このリストにはまだなにもありません。",
   "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
-  "empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
+  "empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう",
   "follow_request.authorize": "許可",
   "follow_request.reject": "拒否",
   "getting_started.appsshort": "アプリ",
@@ -141,6 +143,7 @@
   "mute_modal.hide_notifications": "このユーザーからの通知を隠しますか?",
   "navigation_bar.blocks": "ブロックしたユーザー",
   "navigation_bar.community_timeline": "ローカルタイムライン",
+  "navigation_bar.direct": "ダイレクトメッセージ",
   "navigation_bar.edit_profile": "プロフィールを編集",
   "navigation_bar.favourites": "お気に入り",
   "navigation_bar.follow_requests": "フォローリクエスト",
@@ -159,12 +162,12 @@
   "notifications.clear": "通知を消去",
   "notifications.clear_confirmation": "本当に通知を消去しますか?",
   "notifications.column_settings.alert": "デスクトップ通知",
-  "notifications.column_settings.favourite": "お気に入り",
-  "notifications.column_settings.follow": "新しいフォロワー",
-  "notifications.column_settings.mention": "返信",
+  "notifications.column_settings.favourite": "お気に入り:",
+  "notifications.column_settings.follow": "新しいフォロワー:",
+  "notifications.column_settings.mention": "返信:",
   "notifications.column_settings.push": "プッシュ通知",
   "notifications.column_settings.push_meta": "このデバイス",
-  "notifications.column_settings.reblog": "ブースト",
+  "notifications.column_settings.reblog": "ブースト:",
   "notifications.column_settings.show": "カラムに表示",
   "notifications.column_settings.sound": "通知音を再生",
   "onboarding.done": "完了",
@@ -173,7 +176,7 @@
   "onboarding.page_four.home": "「ホーム」タイムラインではあなたがフォローしている人の投稿を表示します。",
   "onboarding.page_four.notifications": "「通知」ではあなたへの他の人からの関わりを表示します。",
   "onboarding.page_one.federation": "Mastodonは誰でも参加できるSNSです。",
-  "onboarding.page_one.handle": "あなたは今数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です。",
+  "onboarding.page_one.handle": "今あなたは数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です",
   "onboarding.page_one.welcome": "Mastodonへようこそ!",
   "onboarding.page_six.admin": "あなたのインスタンスの管理者は{admin}です。",
   "onboarding.page_six.almost_done": "以上です。",
@@ -181,7 +184,7 @@
   "onboarding.page_six.apps_available": "iOS、Androidあるいは他のプラットフォームで使える{apps}があります。",
   "onboarding.page_six.github": "MastodonはOSSです。バグ報告や機能要望あるいは貢献を{github}から行なえます。",
   "onboarding.page_six.guidelines": "コミュニティガイドライン",
-  "onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください。",
+  "onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください!",
   "onboarding.page_six.various_app": "様々なモバイルアプリ",
   "onboarding.page_three.profile": "「プロフィールを編集」から、あなたの自己紹介や表示名を変更できます。またそこでは他の設定ができます。",
   "onboarding.page_three.search": "検索バーで、{illustration}や{introductions}のように特定のハッシュタグの投稿を見たり、ユーザーを探したりできます。",
@@ -212,7 +215,7 @@
   "search_popout.tips.text": "表示名やユーザー名、ハッシュタグに一致する単純なテキスト",
   "search_popout.tips.user": "ユーザー",
   "search_results.total": "{count, number}件の結果",
-  "standalone.public_title": "今こんな話をしています",
+  "standalone.public_title": "今こんな話をしています...",
   "status.cannot_reblog": "この投稿はブーストできません",
   "status.delete": "削除",
   "status.embed": "埋め込み",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 3f47baa76..472a52a99 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "불러오는 중...",
   "media_gallery.toggle_visible": "표시 전환",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 26e86308d..e154d1ab2 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -36,7 +36,7 @@
   "column.favourites": "Favorieten",
   "column.follow_requests": "Volgverzoeken",
   "column.home": "Start",
-  "column.lists": "Lists",
+  "column.lists": "Lijsten",
   "column.mutes": "Genegeerde gebruikers",
   "column.notifications": "Meldingen",
   "column.pins": "Vastgezette toots",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Verwijderen",
   "confirmations.delete.message": "Weet je het zeker dat je deze toot wilt verwijderen?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Weet je zeker dat je deze lijst definitief wilt verwijderen?",
   "confirmations.domain_block.confirm": "Negeer alles van deze server",
   "confirmations.domain_block.message": "Weet je het echt, echt zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gewenst.",
   "confirmations.mute.confirm": "Negeren",
@@ -91,7 +91,7 @@
   "empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.",
   "empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.",
   "empty_column.home.public_timeline": "de globale tijdlijn",
-  "empty_column.list": "Er is nog niks in deze lijst.",
+  "empty_column.list": "Er is nog niks in deze lijst. Wanneer lijstleden nieuwe toots publiceren, zijn deze hier te zien.",
   "empty_column.notifications": "Je hebt nog geen meldingen. Heb interactie met andere mensen om het gesprek aan te gaan.",
   "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere servers om het te vullen",
   "follow_request.authorize": "Goedkeuren",
@@ -107,34 +107,34 @@
   "home.column_settings.show_reblogs": "Boosts tonen",
   "home.column_settings.show_replies": "Reacties tonen",
   "home.settings": "Kolom-instellingen",
-  "keyboard_shortcuts.back": "om terug te navigeren",
+  "keyboard_shortcuts.back": "om terug te gaan",
   "keyboard_shortcuts.boost": "om te boosten",
-  "keyboard_shortcuts.column": "om te focussen op een status in één van de kolommen",
-  "keyboard_shortcuts.compose": "om te focussen op het toot tekstvak",
-  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.column": "om op een toot te focussen in één van de kolommen",
+  "keyboard_shortcuts.compose": "om het tekstvak voor toots te focussen",
+  "keyboard_shortcuts.description": "Omschrijving",
   "keyboard_shortcuts.down": "om naar beneden door de lijst te bewegen",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "om het te markeren als favoriet",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.favourite": "om als favoriet te markeren",
+  "keyboard_shortcuts.heading": "Sneltoetsen",
   "keyboard_shortcuts.hotkey": "Sneltoets",
   "keyboard_shortcuts.legend": "om deze legenda weer te geven",
   "keyboard_shortcuts.mention": "om de auteur te vermelden",
-  "keyboard_shortcuts.reply": "om te antwoorden",
-  "keyboard_shortcuts.search": "om te focussen op zoeken",
+  "keyboard_shortcuts.reply": "om te reageren",
+  "keyboard_shortcuts.search": "om het zoekvak te focussen",
   "keyboard_shortcuts.toot": "om een nieuwe toot te starten",
-  "keyboard_shortcuts.unfocus": "om te ontfocussen van het toot tekstvak/zoeken",
+  "keyboard_shortcuts.unfocus": "om het tekst- en zoekvak te ontfocussen",
   "keyboard_shortcuts.up": "om omhoog te bewegen in de lijst",
   "lightbox.close": "Sluiten",
   "lightbox.next": "Volgende",
   "lightbox.previous": "Vorige",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
-  "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
-  "lists.subheading": "Your lists",
+  "lists.account.add": "Aan lijst toevoegen",
+  "lists.account.remove": "Uit lijst verwijderen",
+  "lists.delete": "Lijst verwijderen",
+  "lists.edit": "Lijst bewerken",
+  "lists.new.create": "Lijst toevoegen",
+  "lists.new.title_placeholder": "Naam nieuwe lijst",
+  "lists.search": "Zoek naar mensen die je volgt",
+  "lists.subheading": "Jouw lijsten",
   "loading_indicator.label": "Laden…",
   "media_gallery.toggle_visible": "Media wel/niet tonen",
   "missing_indicator.label": "Niet gevonden",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Volgverzoeken",
   "navigation_bar.info": "Uitgebreide informatie",
   "navigation_bar.keyboard_shortcuts": "Toetsenbord sneltoetsen",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Lijsten",
   "navigation_bar.logout": "Afmelden",
   "navigation_bar.mutes": "Genegeerde gebruikers",
   "navigation_bar.pins": "Vastgezette toots",
@@ -204,7 +204,7 @@
   "reply_indicator.cancel": "Annuleren",
   "report.placeholder": "Extra opmerkingen",
   "report.submit": "Verzenden",
-  "report.target": "Rapporteren van",
+  "report.target": "Rapporteer {target}",
   "search.placeholder": "Zoeken",
   "search_popout.search_format": "Geavanceerd zoeken",
   "search_popout.tips.hashtag": "hashtag",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 233b6c946..bf2b6259a 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Laster...",
   "media_gallery.toggle_visible": "Veksle synlighet",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index ec7202ff6..0d1f7c971 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -10,22 +10,22 @@
   "account.hide_reblogs": "Rescondre los partages de @{name}",
   "account.media": "Mèdias",
   "account.mention": "Mencionar @{name}",
-  "account.moved_to": "{name} a mudat los catons a : ",
+  "account.moved_to": "{name} a mudat los catons a :",
   "account.mute": "Rescondre @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Rescondre las notificacions de @{name}",
   "account.posts": "Estatuts",
   "account.report": "Senhalar @{name}",
-  "account.requested": "Invitacion mandada. Clicatz per anullar.",
+  "account.requested": "Invitacion mandada. Clicatz per anullar",
   "account.share": "Partejar lo perfil a @{name}",
   "account.show_reblogs": "Mostrar los partages de @{name}",
   "account.unblock": "Desblocar @{name}",
   "account.unblock_domain": "Desblocar {domain}",
   "account.unfollow": "Quitar de sègre",
   "account.unmute": "Quitar de rescondre @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "Mostrar las notificacions de @{name}",
   "account.view_full_profile": "Veire lo perfil complet",
   "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
-  "bundle_column_error.body": "Quicòm a fach meuca pendent lo cargament d’aqueste compausant.",
+  "bundle_column_error.body": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.",
   "bundle_column_error.retry": "Tornar ensajar",
   "bundle_column_error.title": "Error de ret",
   "bundle_modal_error.close": "Tampar",
@@ -36,7 +36,7 @@
   "column.favourites": "Favorits",
   "column.follow_requests": "Demandas d’abonament",
   "column.home": "Acuèlh",
-  "column.lists": "Lists",
+  "column.lists": "Listas",
   "column.mutes": "Personas rescondudas",
   "column.notifications": "Notificacions",
   "column.pins": "Tuts penjats",
@@ -63,8 +63,8 @@
   "confirmations.block.message": "Sètz segur de voler blocar {name} ?",
   "confirmations.delete.confirm": "Escafar",
   "confirmations.delete.message": "Sètz segur de voler escafar l’estatut ?",
-  "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.confirm": "Suprimir",
+  "confirmations.delete_list.message": "Sètz segur de voler suprimir aquesta lista per totjorn ?",
   "confirmations.domain_block.confirm": "Amagar tot lo domeni",
   "confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
   "confirmations.mute.confirm": "Rescondre",
@@ -72,7 +72,7 @@
   "confirmations.unfollow.confirm": "Quitar de sègre",
   "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
   "embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
-  "embed.preview": "Semblarà aquò : ",
+  "embed.preview": "Semblarà aquò :",
   "emoji_button.activity": "Activitats",
   "emoji_button.custom": "Personalizats",
   "emoji_button.flags": "Drapèus",
@@ -84,16 +84,16 @@
   "emoji_button.people": "Gents",
   "emoji_button.recent": "Sovent utilizats",
   "emoji_button.search": "Cercar…",
-  "emoji_button.search_results": "Resultat de recèrca",
+  "emoji_button.search_results": "Resultats de recèrca",
   "emoji_button.symbols": "Simbòls",
   "emoji_button.travel": "Viatges & lòcs",
   "empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
-  "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
+  "empty_column.hashtag": "I a pas encara de contengut ligat a aquesta etiqueta.",
   "empty_column.home": "Vòstre flux d’acuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
   "empty_column.home.public_timeline": "lo flux public",
-  "empty_column.list": "I a pas res dins la lista pel moment.",
+  "empty_column.list": "I a pas res dins la lista pel moment. Quand de membres d’aquesta lista publiquen de novèls estatuts los veiretz aquí.",
   "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
-  "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.",
+  "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Regetar",
   "getting_started.appsshort": "Apps",
@@ -116,7 +116,7 @@
   "keyboard_shortcuts.enter": "per dobrir los estatuts",
   "keyboard_shortcuts.favourite": "per apondre als favorits",
   "keyboard_shortcuts.heading": "Acorchis clavièr",
-  "keyboard_shortcuts.hotkey": "Clau",
+  "keyboard_shortcuts.hotkey": "Acorchis",
   "keyboard_shortcuts.legend": "per mostrar aquesta legenda",
   "keyboard_shortcuts.mention": "per mencionar l’autor",
   "keyboard_shortcuts.reply": "per respondre",
@@ -127,35 +127,35 @@
   "lightbox.close": "Tampar",
   "lightbox.next": "Seguent",
   "lightbox.previous": "Precedent",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
-  "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
-  "lists.subheading": "Your lists",
+  "lists.account.add": "Ajustar a la lista",
+  "lists.account.remove": "Levar de la lista",
+  "lists.delete": "Suprimir la lista",
+  "lists.edit": "Modificar la lista",
+  "lists.new.create": "Ajustar una lista",
+  "lists.new.title_placeholder": "Títol de la nòva lista",
+  "lists.search": "Cercar demest lo monde que seguètz",
+  "lists.subheading": "Vòstras listas",
   "loading_indicator.label": "Cargament…",
   "media_gallery.toggle_visible": "Modificar la visibilitat",
   "missing_indicator.label": "Pas trobat",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Rescondre las notificacions d’aquesta persona ?",
   "navigation_bar.blocks": "Personas blocadas",
   "navigation_bar.community_timeline": "Flux public local",
   "navigation_bar.edit_profile": "Modificar lo perfil",
   "navigation_bar.favourites": "Favorits",
-  "navigation_bar.follow_requests": "Demandas d'abonament",
+  "navigation_bar.follow_requests": "Demandas d’abonament",
   "navigation_bar.info": "Mai informacions",
   "navigation_bar.keyboard_shortcuts": "Acorchis clavièr",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Listas",
   "navigation_bar.logout": "Desconnexion",
   "navigation_bar.mutes": "Personas rescondudas",
   "navigation_bar.pins": "Tuts penjats",
   "navigation_bar.preferences": "Preferéncias",
   "navigation_bar.public_timeline": "Flux public global",
-  "notification.favourite": "{name} a ajustat a sos favorits :",
+  "notification.favourite": "{name} a ajustat a sos favorits",
   "notification.follow": "{name} vos sèc",
-  "notification.mention": "{name} vos a mencionat :",
-  "notification.reblog": "{name} a partejat vòstre estatut :",
+  "notification.mention": "{name} vos a mencionat",
+  "notification.reblog": "{name} a partejat vòstre estatut",
   "notifications.clear": "Escafar",
   "notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?",
   "notifications.column_settings.alert": "Notificacions localas",
@@ -171,7 +171,7 @@
   "onboarding.next": "Seguent",
   "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
   "onboarding.page_four.home": "Lo flux d’acuèlh mòstra los estatuts del mond que seguètz.",
-  "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos",
+  "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos.",
   "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per construire un malhum mai larg. Òm los apèla instàncias.",
   "onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
   "onboarding.page_one.welcome": "Benvengut a Mastodon !",
@@ -209,7 +209,7 @@
   "search_popout.search_format": "Format recèrca avançada",
   "search_popout.tips.hashtag": "etiqueta",
   "search_popout.tips.status": "estatut",
-  "search_popout.tips.text": "Tèxt brut tòrna escais, noms d’utilizaire e etiquetas correspondents",
+  "search_popout.tips.text": "Lo tèxt brut tòrna escais, noms d’utilizaire e etiquetas correspondents",
   "search_popout.tips.user": "utilizaire",
   "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
   "standalone.public_title": "Una ulhada dedins…",
@@ -225,7 +225,7 @@
   "status.open": "Desplegar aqueste estatut",
   "status.pin": "Penjar al perfil",
   "status.reblog": "Partejar",
-  "status.reblogged_by": "{name} a partejat :",
+  "status.reblogged_by": "{name} a partejat",
   "status.reply": "Respondre",
   "status.replyAll": "Respondre a la conversacion",
   "status.report": "Senhalar @{name}",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 1df27d536..70632846c 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -36,7 +36,7 @@
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores pendentes",
   "column.home": "Página inicial",
-  "column.lists": "Lists",
+  "column.lists": "Listas",
   "column.mutes": "Usuários silenciados",
   "column.notifications": "Notificações",
   "column.pins": "Postagens fixadas",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Excluir",
   "confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Você tem certeza que quer deletar permanentemente a lista?",
   "confirmations.domain_block.confirm": "Esconder o domínio inteiro",
   "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.",
   "confirmations.mute.confirm": "Silenciar",
@@ -109,44 +109,44 @@
   "home.settings": "Configurações de colunas",
   "keyboard_shortcuts.back": "para navegar de volta",
   "keyboard_shortcuts.boost": "para compartilhar",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.column": "Focar um status em uma das colunas",
+  "keyboard_shortcuts.compose": "para focar a área de redação",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "para mover para baixo na lista",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "para adicionar aos favoritos",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.hotkey": "Atalho",
+  "keyboard_shortcuts.legend": "para mostrar essa legenda",
+  "keyboard_shortcuts.mention": "para mencionar o autor",
+  "keyboard_shortcuts.reply": "para responder",
+  "keyboard_shortcuts.search": "para focar a pesquisa",
+  "keyboard_shortcuts.toot": "para compor um novo toot",
+  "keyboard_shortcuts.unfocus": "para remover o foco da área de composição/pesquisa",
+  "keyboard_shortcuts.up": "para mover para cima na lista",
   "lightbox.close": "Fechar",
   "lightbox.next": "Próximo",
   "lightbox.previous": "Anterior",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Adicionar a listas",
+  "lists.account.remove": "Remover da lista",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Editar lista",
+  "lists.new.create": "Adicionar lista",
+  "lists.new.title_placeholder": "Novo título da lista",
+  "lists.search": "Procurar entre as pessoas que você segue",
+  "lists.subheading": "Suas listas",
   "loading_indicator.label": "Carregando...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Esconder notificações deste usuário?",
   "navigation_bar.blocks": "Usuários bloqueados",
   "navigation_bar.community_timeline": "Local",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritos",
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
+  "navigation_bar.lists": "Listas",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Usuários silenciados",
   "navigation_bar.pins": "Postagens fixadas",
@@ -177,7 +177,7 @@
   "onboarding.page_one.welcome": "Seja bem-vindo(a) ao Mastodon!",
   "onboarding.page_six.admin": "O administrador de sua instância é {admin}.",
   "onboarding.page_six.almost_done": "Quase acabando...",
-  "onboarding.page_six.appetoot": "Bon Appetoot!",
+  "onboarding.page_six.appetoot": "Bom Apetoot!",
   "onboarding.page_six.apps_available": "Há {apps} disponíveis para iOS, Android e outras plataformas.",
   "onboarding.page_six.github": "Mastodon é um software gratuito e de código aberto. Você pode reportar bugs, prequisitar novas funções ou contribuir para o código no {github}.",
   "onboarding.page_six.guidelines": "diretrizes da comunidade",
@@ -220,7 +220,7 @@
   "status.load_more": "Carregar mais",
   "status.media_hidden": "Mídia escondida",
   "status.mention": "Mencionar @{name}",
-  "status.more": "More",
+  "status.more": "Mais",
   "status.mute_conversation": "Silenciar conversa",
   "status.open": "Expandir",
   "status.pin": "Fixar no perfil",
@@ -241,7 +241,7 @@
   "tabs_bar.home": "Página inicial",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar mídia",
   "upload_form.description": "Descreva a imagem para deficientes visuais",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 3d3e19571..15d5deb93 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -1,7 +1,7 @@
 {
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Esconder tudo do domínio {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de forma incompleta.",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
@@ -19,7 +19,7 @@
   "account.share": "Partilhar o perfil @{name}",
   "account.show_reblogs": "Mostrar partilhas de @{name}",
   "account.unblock": "Não bloquear @{name}",
-  "account.unblock_domain": "Unhide {domain}",
+  "account.unblock_domain": "Mostrar {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
   "account.unmute_notifications": "Deixar de silenciar @{name}",
@@ -36,7 +36,7 @@
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores Pendentes",
   "column.home": "Home",
-  "column.lists": "Lists",
+  "column.lists": "Listas",
   "column.mutes": "Utilizadores silenciados",
   "column.notifications": "Notificações",
   "column.pins": "Pinned toot",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Eliminar",
   "confirmations.delete.message": "De certeza que queres eliminar esta publicação?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Tens a certeza de que desejas apagar permanentemente esta lista?",
   "confirmations.domain_block.confirm": "Esconder tudo deste domínio",
   "confirmations.domain_block.message": "De certeza que queres bloquear por completo o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é o suficiente e o recomendado.",
   "confirmations.mute.confirm": "Silenciar",
@@ -88,12 +88,12 @@
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viagens & Lugares",
   "empty_column.community": "Ainda não existe conteúdo local para mostrar!",
-  "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag",
+  "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.",
   "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
   "empty_column.home.public_timeline": "global",
   "empty_column.list": "Ainda não existem publicações nesta lista.",
   "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
-  "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
+  "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Rejeitar",
   "getting_started.appsshort": "Aplicações",
@@ -116,7 +116,7 @@
   "keyboard_shortcuts.enter": "para expandir uma publicação",
   "keyboard_shortcuts.favourite": "para adicionar aos favoritos",
   "keyboard_shortcuts.heading": "Atalhos do teclado",
-  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.hotkey": "Atalho",
   "keyboard_shortcuts.legend": "para mostrar esta legenda",
   "keyboard_shortcuts.mention": "para mencionar o autor",
   "keyboard_shortcuts.reply": "para responder",
@@ -127,14 +127,14 @@
   "lightbox.close": "Fechar",
   "lightbox.next": "Próximo",
   "lightbox.previous": "Anterior",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Adicionar à lista",
+  "lists.account.remove": "Remover da lista",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Editar lista",
+  "lists.new.create": "Adicionar lista",
+  "lists.new.title_placeholder": "Novo título da lista",
+  "lists.search": "Pesquisa entre as pessoas que segues",
+  "lists.subheading": "As tuas listas",
   "loading_indicator.label": "A carregar...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
   "navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Listas",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Utilizadores silenciados",
   "navigation_bar.pins": "Posts fixos",
@@ -209,13 +209,13 @@
   "search_popout.search_format": "Formato avançado de pesquisa",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags",
   "search_popout.tips.user": "utilizador",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "standalone.public_title": "Espreitar lá dentro...",
   "status.cannot_reblog": "Este post não pode ser partilhado",
   "status.delete": "Eliminar",
-  "status.embed": "Embed",
+  "status.embed": "Incorporar",
   "status.favourite": "Adicionar aos favoritos",
   "status.load_more": "Carregar mais",
   "status.media_hidden": "Media escondida",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 0aef2d9df..e9925b675 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Загрузка...",
   "media_gallery.toggle_visible": "Показать/скрыть",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 53090452f..9d9646509 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Laddar...",
   "media_gallery.toggle_visible": "Växla synlighet",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 2f064a193..cc18a6096 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Loading...",
   "media_gallery.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index be8103d1c..c51f3e417 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Yükleniyor...",
   "media_gallery.toggle_visible": "Görünürlüğü değiştir",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 273661462..86c0ce76d 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Завантаження...",
   "media_gallery.toggle_visible": "Показати/приховати",
diff --git a/app/javascript/mastodon/locales/whitelist_gl.json b/app/javascript/mastodon/locales/whitelist_gl.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_gl.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index cb5607cc5..9be6a9f73 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -25,11 +25,11 @@
   "account.unmute_notifications": "不再隐藏来自 @{name} 的通知",
   "account.view_full_profile": "查看完整资料",
   "boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
-  "bundle_column_error.body": "载入组件出错。",
+  "bundle_column_error.body": "载入这个组件时发生了错误。",
   "bundle_column_error.retry": "重试",
   "bundle_column_error.title": "网络错误",
   "bundle_modal_error.close": "关闭",
-  "bundle_modal_error.message": "载入组件出错。",
+  "bundle_modal_error.message": "载入这个组件时发生了错误。",
   "bundle_modal_error.retry": "重试",
   "column.blocks": "屏蔽用户",
   "column.community": "本站时间轴",
@@ -50,8 +50,8 @@
   "column_header.unpin": "取消固定",
   "column_subheading.navigation": "导航",
   "column_subheading.settings": "设置",
-  "compose_form.lock_disclaimer": "你的帐户没有{locked}。任何人都可以通过关注你来查看仅关注者可见的嘟文。",
-  "compose_form.lock_disclaimer.lock": "被保护",
+  "compose_form.lock_disclaimer": "你的帐户没有{locked}。任何人都可以在关注你后立即查看仅关注者可见的嘟文。",
+  "compose_form.lock_disclaimer.lock": "开启保护",
   "compose_form.placeholder": "在想啥?",
   "compose_form.publish": "嘟嘟",
   "compose_form.publish_loud": "{publish}!",
@@ -60,17 +60,17 @@
   "compose_form.spoiler_placeholder": "折叠部分的警告消息",
   "confirmation_modal.cancel": "取消",
   "confirmations.block.confirm": "屏蔽",
-  "confirmations.block.message": "想好了,真的要屏蔽 {name}?",
+  "confirmations.block.message": "你确定要屏蔽 {name} 吗?",
   "confirmations.delete.confirm": "删除",
-  "confirmations.delete.message": "想好了,真的要删除这条嘟文?",
+  "confirmations.delete.message": "你确定要删除这条嘟文吗?",
   "confirmations.delete_list.confirm": "删除",
   "confirmations.delete_list.message": "你确定要永久删除这个列表吗?",
   "confirmations.domain_block.confirm": "隐藏整个网站的内容",
-  "confirmations.domain_block.message": "你真的真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户就应该能满足你的需要了。",
+  "confirmations.domain_block.message": "你真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户应该就能满足你的需要了。",
   "confirmations.mute.confirm": "隐藏",
-  "confirmations.mute.message": "想好了,真的要隐藏 {name}?",
+  "confirmations.mute.message": "你确定要隐藏 {name} 吗?",
   "confirmations.unfollow.confirm": "取消关注",
-  "confirmations.unfollow.message": "确定要取消关注 {name} 吗?",
+  "confirmations.unfollow.message": "你确定要取消关注 {name} 吗?",
   "embed.instructions": "要在你的网站上嵌入这条嘟文,请复制以下代码。",
   "embed.preview": "它会像这样显示出来:",
   "emoji_button.activity": "活动",
@@ -91,9 +91,9 @@
   "empty_column.hashtag": "这个话题标签下暂时没有内容。",
   "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
   "empty_column.home.public_timeline": "公共时间轴",
-  "empty_column.list": "这个列表中暂时没有内容。",
-  "empty_column.notifications": "你还没有收到过通知信息,快向其他用户搭讪吧。",
-  "empty_column.public": "这里神马都没有!写一些公开的嘟文,或者关注其他实例的用户,这里就会有嘟文出现了哦!",
+  "empty_column.list": "这个列表中暂时没有内容。列表中用户所发送的的新嘟文将会在这里显示。",
+  "empty_column.notifications": "你还没有收到过任何通知,快向其他用户搭讪吧。",
+  "empty_column.public": "这里神马都没有!写一些公开的嘟文,或者关注其他实例的用户后,这里就会有嘟文出现了哦!",
   "follow_request.authorize": "同意",
   "follow_request.reject": "拒绝",
   "getting_started.appsshort": "应用",
@@ -138,7 +138,7 @@
   "loading_indicator.label": "加载中……",
   "media_gallery.toggle_visible": "切换显示/隐藏",
   "missing_indicator.label": "找不到内容",
-  "mute_modal.hide_notifications": "隐藏来自这个用户的通知",
+  "mute_modal.hide_notifications": "同时隐藏来自这个用户的通知",
   "navigation_bar.blocks": "被屏蔽的用户",
   "navigation_bar.community_timeline": "本站时间轴",
   "navigation_bar.edit_profile": "修改个人资料",
@@ -157,25 +157,25 @@
   "notification.mention": "{name} 提及你",
   "notification.reblog": "{name} 转嘟了你的嘟文",
   "notifications.clear": "清空通知列表",
-  "notifications.clear_confirmation": "你确定要清空通知列表吗?",
+  "notifications.clear_confirmation": "你确定要永久清空通知列表吗?",
   "notifications.column_settings.alert": "桌面通知",
-  "notifications.column_settings.favourite": "你的嘟文被收藏:",
-  "notifications.column_settings.follow": "关注你:",
-  "notifications.column_settings.mention": "提及你:",
+  "notifications.column_settings.favourite": "当你的嘟文被收藏时:",
+  "notifications.column_settings.follow": "当有人关注你时:",
+  "notifications.column_settings.mention": "当有人在嘟文中提及你时:",
   "notifications.column_settings.push": "推送通知",
   "notifications.column_settings.push_meta": "此设备",
-  "notifications.column_settings.reblog": "你的嘟文被转嘟:",
+  "notifications.column_settings.reblog": "当有人转嘟了你的嘟文时:",
   "notifications.column_settings.show": "在通知栏显示",
   "notifications.column_settings.sound": "播放音效",
   "onboarding.done": "出发!",
   "onboarding.next": "下一步",
   "onboarding.page_five.public_timelines": "本站时间轴显示的是由本站({domain})用户发布的所有公开嘟文。跨站公共时间轴显示的的是由本站用户关注对象所发布的所有公开嘟文。这些就是寻人好去处的公共时间轴啦。",
-  "onboarding.page_four.home": "你的主页上的时间轴上显示的是你关注对象的嘟文。",
-  "onboarding.page_four.notifications": "如果有人与你互动,便会出现在通知栏中哦~",
-  "onboarding.page_one.federation": "Mastodon 是由一系列独立的服务器共同打造的强大的社交网络,我们将这些各自独立但又相互连接的服务器叫做实例。",
-  "onboarding.page_one.handle": "你在 {domain},{handle} 就是你的完整帐户名称。",
+  "onboarding.page_four.home": "你的主页时间轴上显示的是你的关注对象所发布的嘟文。",
+  "onboarding.page_four.notifications": "如果有人与你互动了,他们就会出现在通知栏中哦~",
+  "onboarding.page_one.federation": "Mastodon 是由一系列独立的服务器共同打造的强大的社交网络,我们将这些各自独立而又相互连接的服务器叫做实例。",
+  "onboarding.page_one.handle": "你是在 {domain} 上注册的,所以你的完整用户地址是 {handle}。",
   "onboarding.page_one.welcome": "欢迎来到 Mastodon!",
-  "onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.",
+  "onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员。",
   "onboarding.page_six.almost_done": "差不多了……",
   "onboarding.page_six.appetoot": "嗷呜~",
   "onboarding.page_six.apps_available": "我们还有适用于 iOS、Android 和其它平台的{apps}哦~",
@@ -184,8 +184,8 @@
   "onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的{guidelines}!",
   "onboarding.page_six.various_app": "移动设备应用",
   "onboarding.page_three.profile": "你可以修改你的个人资料,比如头像、简介和昵称等偏好设置。",
-  "onboarding.page_three.search": "你可以通过搜索功能寻找用户和话题标签,比如{illustration}或者{introductions}。如果你想搜索其他实例上的用户,就需要输入完整帐户名称(用户名@域名)哦。",
-  "onboarding.page_two.compose": "在撰写栏中开始嘟嘟吧!下方的按钮分别用来上传图片,修改嘟文可见范围,以及添加警告信息。",
+  "onboarding.page_three.search": "你可以通过搜索功能寻找用户和话题标签,比如{illustration}或者{introductions}。如果你想搜索其他实例上的用户,就需要输入完整用户地址(@用户名@域名)哦。",
+  "onboarding.page_two.compose": "在撰写栏中开始嘟嘟吧!下方的按钮分别可以用来上传图片、修改嘟文可见范围,以及添加警告信息。",
   "onboarding.skip": "跳过",
   "privacy.change": "设置嘟文可见范围",
   "privacy.direct.long": "只有被提及的用户能看到",
@@ -196,11 +196,11 @@
   "privacy.public.short": "公开",
   "privacy.unlisted.long": "所有人可见,但不会出现在公共时间轴上",
   "privacy.unlisted.short": "不公开",
-  "relative_time.days": "{number} 天",
-  "relative_time.hours": "{number} 时",
+  "relative_time.days": "{number}天",
+  "relative_time.hours": "{number}时",
   "relative_time.just_now": "刚刚",
-  "relative_time.minutes": "{number} 分",
-  "relative_time.seconds": "{number} 秒",
+  "relative_time.minutes": "{number}分",
+  "relative_time.seconds": "{number}秒",
   "reply_indicator.cancel": "取消",
   "report.placeholder": "附言",
   "report.submit": "提交",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index dbb9584c6..15a68c915 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "載入中...",
   "media_gallery.toggle_visible": "打開或關上",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 0b05a83cd..1bdc883a8 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "讀取中...",
   "media_gallery.toggle_visible": "切換可見性",
diff --git a/app/javascript/mastodon/middleware/sounds.js b/app/javascript/mastodon/middleware/sounds.js
index 3d1e3eaba..9f1bc02b9 100644
--- a/app/javascript/mastodon/middleware/sounds.js
+++ b/app/javascript/mastodon/middleware/sounds.js
@@ -15,7 +15,7 @@ const play = audio => {
     if (typeof audio.fastSeek === 'function') {
       audio.fastSeek(0);
     } else {
-      audio.seek(0);
+      audio.currentTime = 0;
     }
   }
 
diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js
index c4aeb338f..6c5f33557 100644
--- a/app/javascript/mastodon/reducers/status_lists.js
+++ b/app/javascript/mastodon/reducers/status_lists.js
@@ -1,6 +1,10 @@
 import {
+  FAVOURITED_STATUSES_FETCH_REQUEST,
   FAVOURITED_STATUSES_FETCH_SUCCESS,
+  FAVOURITED_STATUSES_FETCH_FAIL,
+  FAVOURITED_STATUSES_EXPAND_REQUEST,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
+  FAVOURITED_STATUSES_EXPAND_FAIL,
 } from '../actions/favourites';
 import {
   PINNED_STATUSES_FETCH_SUCCESS,
@@ -30,6 +34,7 @@ const normalizeList = (state, listType, statuses, next) => {
   return state.update(listType, listMap => listMap.withMutations(map => {
     map.set('next', next);
     map.set('loaded', true);
+    map.set('isLoading', false);
     map.set('items', ImmutableList(statuses.map(item => item.id)));
   }));
 };
@@ -37,6 +42,7 @@ const normalizeList = (state, listType, statuses, next) => {
 const appendToList = (state, listType, statuses, next) => {
   return state.update(listType, listMap => listMap.withMutations(map => {
     map.set('next', next);
+    map.set('isLoading', false);
     map.set('items', map.get('items').concat(statuses.map(item => item.id)));
   }));
 };
@@ -55,6 +61,12 @@ const removeOneFromList = (state, listType, status) => {
 
 export default function statusLists(state = initialState, action) {
   switch(action.type) {
+  case FAVOURITED_STATUSES_FETCH_REQUEST:
+  case FAVOURITED_STATUSES_EXPAND_REQUEST:
+    return state.setIn(['favourites', 'isLoading'], true);
+  case FAVOURITED_STATUSES_FETCH_FAIL:
+  case FAVOURITED_STATUSES_EXPAND_FAIL:
+    return state.setIn(['favourites', 'isLoading'], false);
   case FAVOURITED_STATUSES_FETCH_SUCCESS:
     return normalizeList(state, 'favourites', action.statuses, action.next);
   case FAVOURITED_STATUSES_EXPAND_SUCCESS:
diff --git a/app/javascript/mastodon/settings.js b/app/javascript/mastodon/settings.js
new file mode 100644
index 000000000..dbd969cb1
--- /dev/null
+++ b/app/javascript/mastodon/settings.js
@@ -0,0 +1,46 @@
+export default class Settings {
+
+  constructor(keyBase = null) {
+    this.keyBase = keyBase;
+  }
+
+  generateKey(id) {
+    return this.keyBase ? [this.keyBase, `id${id}`].join('.') : id;
+  }
+
+  set(id, data) {
+    const key = this.generateKey(id);
+    try {
+      const encodedData = JSON.stringify(data);
+      localStorage.setItem(key, encodedData);
+      return data;
+    } catch (e) {
+      return null;
+    }
+  }
+
+  get(id) {
+    const key = this.generateKey(id);
+    try {
+      const rawData = localStorage.getItem(key);
+      return JSON.parse(rawData);
+    } catch (e) {
+      return null;
+    }
+  }
+
+  remove(id) {
+    const data = this.get(id);
+    if (data) {
+      const key = this.generateKey(id);
+      try {
+        localStorage.removeItem(key);
+      } catch (e) {
+      }
+    }
+    return data;
+  }
+
+}
+
+export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js
index 36c68ffc5..9a6f4f26d 100644
--- a/app/javascript/mastodon/stream.js
+++ b/app/javascript/mastodon/stream.js
@@ -62,7 +62,13 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
 
 
 export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
-  const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
+  const params = [ `stream=${stream}` ];
+
+  if (accessToken !== null) {
+    params.push(`access_token=${accessToken}`);
+  }
+
+  const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`);
 
   ws.onopen      = connected;
   ws.onmessage   = e => received(JSON.parse(e.data));
diff --git a/app/javascript/mastodon/web_push_subscription.js b/app/javascript/mastodon/web_push_subscription.js
index 3dbed09ea..17aca4060 100644
--- a/app/javascript/mastodon/web_push_subscription.js
+++ b/app/javascript/mastodon/web_push_subscription.js
@@ -1,6 +1,7 @@
 import axios from 'axios';
 import { store } from './containers/mastodon';
 import { setBrowserSupport, setSubscription, clearSubscription } from './actions/push_notifications';
+import { pushNotificationsSetting } from './settings';
 
 // Taken from https://www.npmjs.com/package/web-push
 const urlBase64ToUint8Array = (base64String) => {
@@ -35,16 +36,33 @@ const subscribe = (registration) =>
 const unsubscribe = ({ registration, subscription }) =>
   subscription ? subscription.unsubscribe().then(() => registration) : registration;
 
-const sendSubscriptionToBackend = (subscription) =>
-  axios.post('/api/web/push_subscriptions', {
-    subscription,
-  }).then(response => response.data);
+const sendSubscriptionToBackend = (subscription) => {
+  const params = { subscription };
+
+  const me = store.getState().getIn(['meta', 'me']);
+  if (me) {
+    const data = pushNotificationsSetting.get(me);
+    if (data) {
+      params.data = data;
+    }
+  }
+
+  return axios.post('/api/web/push_subscriptions', params).then(response => response.data);
+};
 
 // Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
 const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
 
 export function register () {
   store.dispatch(setBrowserSupport(supportsPushNotifications));
+  const me = store.getState().getIn(['meta', 'me']);
+
+  if (me && !pushNotificationsSetting.get(me)) {
+    const alerts = store.getState().getIn(['push_notifications', 'alerts']);
+    if (alerts) {
+      pushNotificationsSetting.set(me, { alerts: alerts });
+    }
+  }
 
   if (supportsPushNotifications) {
     if (!getApplicationServerKey()) {
@@ -79,6 +97,9 @@ export function register () {
         // it means that the backend subscription is valid (and was set during hydration)
         if (!(subscription instanceof PushSubscription)) {
           store.dispatch(setSubscription(subscription));
+          if (me) {
+            pushNotificationsSetting.set(me, { alerts: subscription.alerts });
+          }
         }
       })
       .catch(error => {
@@ -90,6 +111,9 @@ export function register () {
 
         // Clear alerts and hide UI settings
         store.dispatch(clearSubscription());
+        if (me) {
+          pushNotificationsSetting.remove(me);
+        }
 
         try {
           getRegistration()
diff --git a/app/javascript/skins/vanilla/win95.scss b/app/javascript/skins/vanilla/win95/common.scss
index 298f6ee9d..298f6ee9d 100644
--- a/app/javascript/skins/vanilla/win95.scss
+++ b/app/javascript/skins/vanilla/win95/common.scss
diff --git a/app/javascript/skins/vanilla/win95/names.yml b/app/javascript/skins/vanilla/win95/names.yml
new file mode 100644
index 000000000..2083084a2
--- /dev/null
+++ b/app/javascript/skins/vanilla/win95/names.yml
@@ -0,0 +1,4 @@
+en:
+  skins:
+    vanilla:
+      win95: Masto95
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 17322264e..cf15def68 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -265,198 +265,286 @@
 
 .compose-form {
   padding: 10px;
-}
 
-.compose-form__warning {
-  color: darken($ui-secondary-color, 65%);
-  margin-bottom: 15px;
-  background: $ui-primary-color;
-  box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
-  padding: 8px 10px;
-  border-radius: 4px;
-  font-size: 13px;
-  font-weight: 400;
-
-  strong {
+  .compose-form__warning {
     color: darken($ui-secondary-color, 65%);
-    font-weight: 500;
+    margin-bottom: 15px;
+    background: $ui-primary-color;
+    box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
+    padding: 8px 10px;
+    border-radius: 4px;
+    font-size: 13px;
+    font-weight: 400;
 
-    @each $lang in $cjk-langs {
-      &:lang(#{$lang}) {
-        font-weight: 700;
+    strong {
+      color: darken($ui-secondary-color, 65%);
+      font-weight: 500;
+
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
+    }
+
+    a {
+      color: darken($ui-primary-color, 33%);
+      font-weight: 500;
+      text-decoration: underline;
+
+      &:hover,
+      &:active,
+      &:focus {
+        text-decoration: none;
       }
     }
   }
 
-  a {
-    color: darken($ui-primary-color, 33%);
-    font-weight: 500;
-    text-decoration: underline;
+  .compose-form__autosuggest-wrapper {
+    position: relative;
+
+    .emoji-picker-dropdown {
+      position: absolute;
+      right: 5px;
+      top: 5px;
+    }
+  }
+
+  .autosuggest-textarea,
+  .spoiler-input {
+    position: relative;
+  }
+
+  .autosuggest-textarea__textarea,
+  .spoiler-input__input {
+    display: block;
+    box-sizing: border-box;
+    width: 100%;
+    margin: 0;
+    color: $ui-base-color;
+    background: $simple-background-color;
+    padding: 10px;
+    font-family: inherit;
+    font-size: 14px;
+    resize: vertical;
+    border: 0;
+    outline: 0;
 
-    &:hover,
-    &:active,
     &:focus {
-      text-decoration: none;
+      outline: 0;
+    }
+
+    @media screen and (max-width: 600px) {
+      font-size: 16px;
     }
   }
-}
 
-.compose-form__modifiers {
-  color: $ui-base-color;
-  font-family: inherit;
-  font-size: 14px;
-  background: $simple-background-color;
-  border-radius: 0 0 4px;
-}
+  .spoiler-input__input {
+    border-radius: 4px;
+  }
 
-.compose-form__buttons-wrapper {
-  display: flex;
-  justify-content: space-between;
-}
+  .autosuggest-textarea__textarea {
+    min-height: 100px;
+    border-radius: 4px 4px 0 0;
+    padding-bottom: 0;
+    padding-right: 10px + 22px;
+    resize: none;
 
-.compose-form__buttons {
-  padding: 10px;
-  background: darken($simple-background-color, 8%);
-  box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05);
-  border-radius: 0 0 4px 4px;
-  display: flex;
+    @media screen and (max-width: 600px) {
+      height: 100px !important; // prevent auto-resize textarea
+      resize: vertical;
+    }
+  }
 
-  .icon-button {
-    box-sizing: content-box;
-    padding: 0 3px;
+  .autosuggest-textarea__suggestions {
+    box-sizing: border-box;
+    display: none;
+    position: absolute;
+    top: 100%;
+    width: 100%;
+    z-index: 99;
+    box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
+    background: $ui-secondary-color;
+    border-radius: 0 0 4px 4px;
+    color: $ui-base-color;
+    font-size: 14px;
+    padding: 6px;
+
+    &.autosuggest-textarea__suggestions--visible {
+      display: block;
+    }
   }
-}
 
-.compose-form__upload-button-icon {
-  line-height: 27px;
-}
+  .autosuggest-textarea__suggestions__item {
+    padding: 10px;
+    cursor: pointer;
+    border-radius: 4px;
 
-.compose-form__sensitive-button {
-  display: none;
+    &:hover,
+    &:focus,
+    &:active,
+    &.selected {
+      background: darken($ui-secondary-color, 10%);
+    }
+  }
 
-  &.compose-form__sensitive-button--visible {
+  .autosuggest-account,
+  .autosuggest-emoji {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: flex-start;
+    line-height: 18px;
+    font-size: 14px;
+  }
+
+  .autosuggest-account-icon,
+  .autosuggest-emoji img {
     display: block;
+    margin-right: 8px;
+    width: 16px;
+    height: 16px;
   }
 
-  .compose-form__sensitive-button__icon {
-    line-height: 27px;
+  .autosuggest-account .display-name__account {
+    color: lighten($ui-base-color, 36%);
   }
-}
 
-.compose-form__upload-wrapper {
-  overflow: hidden;
-}
+  .compose-form__modifiers {
+    color: $ui-base-color;
+    font-family: inherit;
+    font-size: 14px;
+    background: $simple-background-color;
 
-.compose-form__uploads-wrapper {
-  display: flex;
-  flex-direction: row;
-  padding: 5px;
-  flex-wrap: wrap;
-}
+    .compose-form__upload-wrapper {
+      overflow: hidden;
+    }
 
-.compose-form__upload {
-  flex: 1 1 0;
-  min-width: 40%;
-  margin: 5px;
+    .compose-form__uploads-wrapper {
+      display: flex;
+      flex-direction: row;
+      padding: 5px;
+      flex-wrap: wrap;
+    }
 
-  &-description {
-    position: absolute;
-    z-index: 2;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    box-sizing: border-box;
-    background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
-    padding: 10px;
-    opacity: 0;
-    transition: opacity .1s ease;
+    .compose-form__upload {
+      flex: 1 1 0;
+      min-width: 40%;
+      margin: 5px;
 
-    input {
-      background: transparent;
-      color: $ui-secondary-color;
-      border: 0;
-      padding: 0;
-      margin: 0;
-      width: 100%;
-      font-family: inherit;
-      font-size: 14px;
-      font-weight: 500;
+      &-description {
+        position: absolute;
+        z-index: 2;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        box-sizing: border-box;
+        background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
+        padding: 10px;
+        opacity: 0;
+        transition: opacity .1s ease;
+
+        input {
+          background: transparent;
+          color: $ui-secondary-color;
+          border: 0;
+          padding: 0;
+          margin: 0;
+          width: 100%;
+          font-family: inherit;
+          font-size: 14px;
+          font-weight: 500;
+
+          &:focus {
+            color: $white;
+          }
 
-      &:focus {
-        color: $white;
+          &::placeholder {
+            opacity: 0.54;
+            color: $ui-secondary-color;
+          }
+        }
+
+        &.active {
+          opacity: 1;
+        }
       }
 
-      &::placeholder {
-        opacity: 0.54;
-        color: $ui-secondary-color;
+      .icon-button {
+        mix-blend-mode: difference;
       }
     }
 
-    &.active {
-      opacity: 1;
+    .compose-form__upload-thumbnail {
+      border-radius: 4px;
+      background-position: center;
+      background-size: cover;
+      background-repeat: no-repeat;
+      height: 100px;
+      width: 100%;
     }
   }
 
-  .icon-button {
-    mix-blend-mode: difference;
-  }
-}
+  .compose-form__buttons-wrapper {
+    padding: 10px;
+    background: darken($simple-background-color, 8%);
+    border-radius: 0 0 4px 4px;
+    display: flex;
+    justify-content: space-between;
 
-.compose-form__upload-thumbnail {
-  border-radius: 4px;
-  background-position: center;
-  background-size: cover;
-  background-repeat: no-repeat;
-  height: 100px;
-  width: 100%;
-}
+    .compose-form__buttons {
+      display: flex;
 
-.compose-form__label {
-  display: block;
-  line-height: 24px;
-  vertical-align: middle;
+      .compose-form__upload-button-icon {
+        line-height: 27px;
+      }
 
-  &.with-border {
-    border-top: 1px solid $ui-base-color;
-    padding-top: 10px;
-  }
+      .compose-form__sensitive-button {
+        display: none;
 
-  .compose-form__label__text {
-    display: inline-block;
-    vertical-align: middle;
-    margin-bottom: 14px;
-    margin-left: 8px;
-    color: $ui-primary-color;
-  }
-}
+        &.compose-form__sensitive-button--visible {
+          display: block;
+        }
 
-.compose-form__textarea,
-.follow-form__input {
-  background: $simple-background-color;
+        .compose-form__sensitive-button__icon {
+          line-height: 27px;
+        }
+      }
+    }
 
-  &:disabled {
-    background: $ui-secondary-color;
-  }
-}
+    .icon-button {
+      box-sizing: content-box;
+      padding: 0 3px;
+    }
 
-.compose-form__autosuggest-wrapper {
-  position: relative;
+    .character-counter__wrapper {
+      align-self: center;
+      margin-right: 4px;
 
-  .emoji-picker-dropdown {
-    position: absolute;
-    right: 5px;
-    top: 5px;
+      .character-counter {
+        cursor: default;
+        font-family: 'mastodon-font-sans-serif', sans-serif;
+        font-size: 14px;
+        font-weight: 600;
+        color: lighten($ui-base-color, 12%);
+
+        &.character-counter--over {
+          color: $warning-red;
+        }
+      }
+    }
   }
-}
 
-.compose-form__publish {
-  display: flex;
-  min-width: 0;
-}
+  .compose-form__publish {
+    display: flex;
+    justify-content: flex-end;
+    min-width: 0;
 
-.compose-form__publish-button-wrapper {
-  overflow: hidden;
-  padding-top: 10px;
+    .compose-form__publish-button-wrapper {
+      overflow: hidden;
+      padding-top: 10px;
+    }
+  }
 }
 
 .emojione {
@@ -518,6 +606,7 @@
   font-weight: 400;
   overflow: hidden;
   white-space: pre-wrap;
+  padding-top: 2px;
 
   &:focus {
     outline: 0;
@@ -616,6 +705,10 @@
 
     .status.status-direct {
       background: lighten($ui-base-color, 12%);
+
+      &.muted {
+        background: transparent;
+      }
     }
 
     .detailed-status,
@@ -770,7 +863,7 @@
 .status__action-bar {
   align-items: center;
   display: flex;
-  margin-top: 10px;
+  margin-top: 8px;
 }
 
 .status__action-bar-button {
@@ -803,7 +896,7 @@
     .emojione {
       width: 24px;
       height: 24px;
-      margin: -3px 0 0;
+      margin: -1px 0 0;
     }
   }
 
@@ -1973,121 +2066,6 @@
   cursor: default;
 }
 
-.autosuggest-textarea,
-.spoiler-input {
-  position: relative;
-}
-
-.autosuggest-textarea__textarea,
-.spoiler-input__input {
-  display: block;
-  box-sizing: border-box;
-  width: 100%;
-  margin: 0;
-  color: $ui-base-color;
-  background: $simple-background-color;
-  padding: 10px;
-  font-family: inherit;
-  font-size: 14px;
-  resize: vertical;
-  border: 0;
-  outline: 0;
-
-  &:focus {
-    outline: 0;
-  }
-
-  @media screen and (max-width: 600px) {
-    font-size: 16px;
-  }
-}
-
-.spoiler-input__input {
-  border-radius: 4px;
-}
-
-.autosuggest-textarea__textarea {
-  min-height: 100px;
-  border-radius: 4px 4px 0 0;
-  padding-bottom: 0;
-  padding-right: 10px + 22px;
-  resize: none;
-
-  @media screen and (max-width: 600px) {
-    height: 100px !important; // prevent auto-resize textarea
-    resize: vertical;
-  }
-}
-
-.autosuggest-textarea__suggestions {
-  box-sizing: border-box;
-  display: none;
-  position: absolute;
-  top: 100%;
-  width: 100%;
-  z-index: 99;
-  box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
-  background: $ui-secondary-color;
-  border-radius: 0 0 4px 4px;
-  color: $ui-base-color;
-  font-size: 14px;
-  padding: 6px;
-
-  &.autosuggest-textarea__suggestions--visible {
-    display: block;
-  }
-}
-
-.autosuggest-textarea__suggestions__item {
-  padding: 10px;
-  cursor: pointer;
-  border-radius: 4px;
-
-  &:hover,
-  &:focus,
-  &:active,
-  &.selected {
-    background: darken($ui-secondary-color, 10%);
-  }
-}
-
-.autosuggest-account,
-.autosuggest-emoji {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: flex-start;
-  line-height: 18px;
-  font-size: 14px;
-}
-
-.autosuggest-account-icon,
-.autosuggest-emoji img {
-  display: block;
-  margin-right: 8px;
-  width: 16px;
-  height: 16px;
-}
-
-.autosuggest-account .display-name__account {
-  color: lighten($ui-base-color, 36%);
-}
-
-.character-counter__wrapper {
-  line-height: 36px;
-  margin: 0 16px 0 8px;
-  padding-top: 10px;
-}
-
-.character-counter {
-  cursor: default;
-  font-size: 16px;
-}
-
-.character-counter--over {
-  color: $warning-red;
-}
-
 .getting-started__wrapper {
   position: relative;
   overflow-y: auto;
@@ -2273,14 +2251,19 @@ button.icon-button.active i.fa-retweet {
   .status-card__image-image {
     border-radius: 4px 4px 0 0;
   }
+
+  .status-card__title {
+    white-space: inherit;
+  }
 }
 
 .status-card__image-image {
   border-radius: 4px 0 0 4px;
   display: block;
-  height: auto;
   margin: 0;
   width: 100%;
+  height: 100%;
+  object-fit: cover;
 }
 
 .load-more {
@@ -3998,6 +3981,7 @@ button.icon-button.active i.fa-retweet {
   position: relative;
   background: $base-shadow-color;
   max-width: 100%;
+  border-radius: 4px;
 
   video {
     height: 100%;
@@ -4032,8 +4016,8 @@ button.icon-button.active i.fa-retweet {
     left: 0;
     right: 0;
     box-sizing: border-box;
-    background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 60%, transparent);
-    padding: 0 10px;
+    background: linear-gradient(0deg, rgba($base-shadow-color, 0.85) 0, rgba($base-shadow-color, 0.45) 60%, transparent);
+    padding: 0 15px;
     opacity: 0;
     transition: opacity .1s ease;
 
@@ -4086,40 +4070,67 @@ button.icon-button.active i.fa-retweet {
     }
   }
 
-  &__buttons {
+  &__buttons-bar {
+    display: flex;
+    justify-content: space-between;
     padding-bottom: 10px;
+  }
+
+  &__buttons {
     font-size: 16px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
 
     &.left {
-      float: left;
-
       button {
-        padding-right: 10px;
+        padding-left: 0;
       }
     }
 
     &.right {
-      float: right;
-
       button {
-        padding-left: 10px;
+        padding-right: 0;
       }
     }
 
     button {
       background: transparent;
-      padding: 0;
+      padding: 2px 10px;
+      font-size: 16px;
       border: 0;
-      color: $white;
+      color: rgba($white, 0.75);
 
       &:active,
       &:hover,
       &:focus {
-        color: $ui-highlight-color;
+        color: $white;
       }
     }
   }
 
+  &__time-sep,
+  &__time-total,
+  &__time-current {
+    font-size: 14px;
+    font-weight: 500;
+  }
+
+  &__time-current {
+    color: $white;
+    margin-left: 10px;
+  }
+
+  &__time-sep {
+    display: inline-block;
+    margin: 0 6px;
+  }
+
+  &__time-sep,
+  &__time-total {
+    color: $white;
+  }
+
   &__seek {
     cursor: pointer;
     height: 24px;
@@ -4129,6 +4140,7 @@ button.icon-button.active i.fa-retweet {
       content: "";
       width: 100%;
       background: rgba($white, 0.35);
+      border-radius: 4px;
       display: block;
       position: absolute;
       height: 4px;
@@ -4140,8 +4152,9 @@ button.icon-button.active i.fa-retweet {
       display: block;
       position: absolute;
       height: 4px;
+      border-radius: 4px;
       top: 10px;
-      background: $ui-highlight-color;
+      background: lighten($ui-highlight-color, 8%);
     }
 
     &__buffer {
@@ -4158,7 +4171,8 @@ button.icon-button.active i.fa-retweet {
       top: 6px;
       margin-left: -6px;
       transition: opacity .1s ease;
-      background: $ui-highlight-color;
+      background: lighten($ui-highlight-color, 8%);
+      box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
       pointer-events: none;
 
       &.active {
@@ -4172,6 +4186,16 @@ button.icon-button.active i.fa-retweet {
       }
     }
   }
+
+  &.detailed,
+  &.fullscreen {
+    .video-player__buttons {
+      button {
+        padding-top: 10px;
+        padding-bottom: 10px;
+      }
+    }
+  }
 }
 
 .media-spoiler-video {
diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss
index 67bfa8a38..77420c84b 100644
--- a/app/javascript/styles/mastodon/rtl.scss
+++ b/app/javascript/styles/mastodon/rtl.scss
@@ -7,9 +7,9 @@ body.rtl {
     margin-left: 5px;
   }
 
-  .character-counter__wrapper {
-    margin-right: 8px;
-    margin-left: 16px;
+  .compose-form .compose-form__buttons-wrapper .character-counter__wrapper {
+    margin-right: 0;
+    margin-left: 4px;
   }
 
   .navigation-bar__profile {
@@ -30,6 +30,22 @@ body.rtl {
   .column-header__buttons {
     left: 0;
     right: auto;
+    margin-left: -15px;
+    margin-right: 0;
+  }
+
+  .column-inline-form .icon-button {
+    margin-left: 0;
+    margin-right: 5px;
+  }
+
+  .column-header__links .text-btn {
+    margin-left: 10px;
+    margin-right: 0;
+  }
+
+  .account__avatar-wrapper {
+    float: right;
   }
 
   .column-header__back-button {
@@ -41,10 +57,6 @@ body.rtl {
     float: left;
   }
 
-  .compose-form__modifiers {
-    border-radius: 0 0 0 4px;
-  }
-
   .setting-toggle {
     margin-left: 0;
     margin-right: 8px;
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 31e0abe39..3a985c19b 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -20,11 +20,13 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   private
 
   def process_status
+    media_attachments = process_attachments
+
     ApplicationRecord.transaction do
       @status = Status.create!(status_params)
 
       process_tags(@status)
-      process_attachments(@status)
+      attach_media(@status, media_attachments)
     end
 
     resolve_thread(@status)
@@ -105,22 +107,36 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     emoji.save
   end
 
-  def process_attachments(status)
+  def process_attachments
     return if @object['attachment'].nil?
 
+    media_attachments = []
+
     as_array(@object['attachment']).each do |attachment|
       next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank?
 
       href             = Addressable::URI.parse(attachment['url']).normalize.to_s
-      media_attachment = MediaAttachment.create(status: status, account: status.account, remote_url: href, description: attachment['name'].presence)
+      media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence)
+      media_attachments << media_attachment
 
       next if skip_download?
 
       media_attachment.file_remote_url = href
       media_attachment.save
     end
+
+    media_attachments
   rescue Addressable::URI::InvalidURIError => e
     Rails.logger.debug e
+
+    media_attachments
+  end
+
+  def attach_media(status, media_attachments)
+    return if media_attachments.blank?
+
+    media = MediaAttachment.where(status_id: nil, id: media_attachments.take(4).map(&:id))
+    media.update(status_id: status.id)
   end
 
   def resolve_thread(status)
diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb
index 3418e2420..f210e134a 100644
--- a/app/lib/ostatus/activity/creation.rb
+++ b/app/lib/ostatus/activity/creation.rb
@@ -26,6 +26,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
     cached_reblog = reblog
     status = nil
 
+    media_attachments = save_media
+
     ApplicationRecord.transaction do
       status = Status.create!(
         uri: id,
@@ -44,7 +46,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
 
       save_mentions(status)
       save_hashtags(status)
-      save_media(status)
+      attach_media(status, media_attachments)
       save_emojis(status)
     end
 
@@ -126,18 +128,20 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
     ProcessHashtagsService.new.call(parent, tags)
   end
 
-  def save_media(parent)
-    do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
+  def save_media
+    do_not_download = DomainBlock.find_by(domain: @account.domain)&.reject_media?
+    media_attachments = []
 
     @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
       next unless link['href']
 
-      media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href'])
+      media = MediaAttachment.where(status: nil, remote_url: link['href']).first_or_initialize(account: @account, status: nil, remote_url: link['href'])
       parsed_url = Addressable::URI.parse(link['href']).normalize
 
       next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty?
 
       media.save
+      media_attachments << media
 
       next if do_not_download
 
@@ -148,6 +152,15 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
         next
       end
     end
+
+    media_attachments
+  end
+
+  def attach_media(parent, media_attachments)
+    return if media_attachments.blank?
+
+    media = MediaAttachment.where(status_id: nil, id: media_attachments.take(4).map(&:id))
+    media.update(status_id: parent.id)
   end
 
   def save_emojis(parent)
diff --git a/app/lib/provider_discovery.rb b/app/lib/provider_discovery.rb
index bcc4ed500..04ba38101 100644
--- a/app/lib/provider_discovery.rb
+++ b/app/lib/provider_discovery.rb
@@ -2,13 +2,26 @@
 
 class ProviderDiscovery < OEmbed::ProviderDiscovery
   class << self
+    def get(url, **options)
+      provider = discover_provider(url, options)
+
+      options.delete(:html)
+
+      provider.get(url, options)
+    end
+
     def discover_provider(url, **options)
-      res    = Request.new(:get, url).perform
       format = options[:format]
 
-      raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
+      if options[:html]
+        html = Nokogiri::HTML(options[:html])
+      else
+        res = Request.new(:get, url).perform
+
+        raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
 
-      html = Nokogiri::HTML(res.to_s)
+        html = Nokogiri::HTML(res.to_s)
+      end
 
       if format.nil? || format == :json
         provider_endpoint ||= html.at_xpath('//link[@type="application/json+oembed"]')&.attribute('href')&.value
diff --git a/app/lib/status_filter.rb b/app/lib/status_filter.rb
index 27e1f9d30..a6a050ce1 100644
--- a/app/lib/status_filter.rb
+++ b/app/lib/status_filter.rb
@@ -35,7 +35,7 @@ class StatusFilter
   end
 
   def silenced_account?
-    status_account_silenced? && !account_following_status_account?
+    !account&.silenced? && status_account_silenced? && !account_following_status_account?
   end
 
   def status_account_silenced?
diff --git a/app/lib/themes.rb b/app/lib/themes.rb
index 863326e2d..55824a5c4 100644
--- a/app/lib/themes.rb
+++ b/app/lib/themes.rb
@@ -14,9 +14,27 @@ class Themes
     result = Hash.new
     Dir.glob(Rails.root.join('app', 'javascript', 'flavours', '*', 'theme.yml')) do |path|
       data = YAML.load_file(path)
-      name = File.basename(File.dirname(path))
+      dir = File.dirname(path)
+      name = File.basename(dir)
+      locales = []
+      screenshots = []
+      if data['locales']
+        Dir.glob(File.join(dir, data['locales'], '*.{js,json}')) do |locale|
+          localeName = File.basename(locale, File.extname(locale))
+          locales.push(localeName) unless localeName.match(/defaultMessages|whitelist|index/)
+        end
+      end
+      if data['screenshot']
+        if data['screenshot'].is_a? Array
+          screenshots = data['screenshot']
+        else
+          screenshots.push(data['screenshot'])
+        end
+      end
       if data['pack']
         data['name'] = name
+        data['locales'] = locales
+        data['screenshot'] = screenshots
         data['skin'] = { 'default' => [] }
         result[name] = data
       end
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index 8af384a2d..5f0176f27 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -21,6 +21,7 @@ class UserSettingsDecorator
     user.settings['default_sensitive']   = default_sensitive_preference if change?('setting_default_sensitive')
     user.settings['unfollow_modal']      = unfollow_modal_preference if change?('setting_unfollow_modal')
     user.settings['boost_modal']         = boost_modal_preference if change?('setting_boost_modal')
+    user.settings['favourite_modal']         = favourite_modal_preference if change?('setting_favourite_modal')
     user.settings['delete_modal']        = delete_modal_preference if change?('setting_delete_modal')
     user.settings['auto_play_gif']       = auto_play_gif_preference if change?('setting_auto_play_gif')
     user.settings['reduce_motion']       = reduce_motion_preference if change?('setting_reduce_motion')
@@ -53,6 +54,10 @@ class UserSettingsDecorator
   def boost_modal_preference
     boolean_cast_setting 'setting_boost_modal'
   end
+  
+  def favourite_modal_preference
+    boolean_cast_setting 'setting_favourite_modal'
+  end
 
   def delete_modal_preference
     boolean_cast_setting 'setting_delete_modal'
diff --git a/app/models/account.rb b/app/models/account.rb
index 48b17bbb8..c75ea028e 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -287,6 +287,7 @@ class Account < ApplicationRecord
         FROM accounts
         WHERE #{query} @@ #{textsearch}
           AND accounts.suspended = false
+          AND accounts.moved_to_account_id IS NULL
         ORDER BY rank DESC
         LIMIT ?
       SQL
@@ -312,6 +313,7 @@ class Account < ApplicationRecord
           WHERE accounts.id IN (SELECT * FROM first_degree)
             AND #{query} @@ #{textsearch}
             AND accounts.suspended = false
+            AND accounts.moved_to_account_id IS NULL
           GROUP BY accounts.id
           ORDER BY rank DESC
           LIMIT ?
@@ -327,6 +329,7 @@ class Account < ApplicationRecord
           LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
           WHERE #{query} @@ #{textsearch}
             AND accounts.suspended = false
+            AND accounts.moved_to_account_id IS NULL
           GROUP BY accounts.id
           ORDER BY rank DESC
           LIMIT ?
diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
index 189872368..dc7a03039 100644
--- a/app/models/account_filter.rb
+++ b/app/models/account_filter.rb
@@ -45,6 +45,8 @@ class AccountFilter
       else
         Account.default_scoped
       end
+    when 'staff'
+      accounts_with_users.merge User.staff
     else
       raise "Unknown filter: #{key}"
     end
diff --git a/app/models/custom_emoji_filter.rb b/app/models/custom_emoji_filter.rb
index 2d1394a59..2c09ed65c 100644
--- a/app/models/custom_emoji_filter.rb
+++ b/app/models/custom_emoji_filter.rb
@@ -27,6 +27,8 @@ class CustomEmojiFilter
       CustomEmoji.remote
     when 'by_domain'
       CustomEmoji.where(domain: value)
+    when 'shortcode'
+      CustomEmoji.where(shortcode: value)
     else
       raise "Unknown filter: #{key}"
     end
diff --git a/app/models/list.rb b/app/models/list.rb
index 910864b26..be85c3b87 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -4,7 +4,7 @@
 # Table name: lists
 #
 #  id         :integer          not null, primary key
-#  account_id :integer
+#  account_id :integer          not null
 #  title      :string           default(""), not null
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
@@ -13,6 +13,8 @@
 class List < ApplicationRecord
   include Paginable
 
+  PER_ACCOUNT_LIMIT = 50
+
   belongs_to :account
 
   has_many :list_accounts, inverse_of: :list, dependent: :destroy
@@ -20,6 +22,10 @@ class List < ApplicationRecord
 
   validates :title, presence: true
 
+  validates_each :account_id, on: :create do |record, _attr, value|
+    record.errors.add(:base, I18n.t('lists.errors.limit')) if List.where(account_id: value).count >= PER_ACCOUNT_LIMIT
+  end
+
   before_destroy :clean_feed_manager
 
   private
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index 5baddba8a..716b82243 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -33,7 +33,7 @@ class PreviewCard < ApplicationRecord
 
   has_and_belongs_to_many :statuses
 
-  has_attached_file :image, styles: { original: '280x280>' }, convert_options: { all: '-quality 80 -strip' }
+  has_attached_file :image, styles: { original: '400x400>' }, convert_options: { all: '-quality 80 -strip' }
 
   include Attachmentable
   include Remotable
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 0fa08e157..dc2c8d129 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -23,7 +23,7 @@ class Tag < ApplicationRecord
 
   class << self
     def search_for(term, limit = 5)
-      pattern = sanitize_sql_like(term) + '%'
+      pattern = sanitize_sql_like(term.strip) + '%'
       Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit)
     end
   end
diff --git a/app/models/user.rb b/app/models/user.rb
index 29bdcbd67..47bf22e17 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -75,7 +75,7 @@ class User < ApplicationRecord
 
   has_many :session_activations, dependent: :destroy
 
-  delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
+  delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
            :reduce_motion, :system_font_ui, :noindex, :flavour, :skin,
            to: :settings, prefix: :setting, allow_nil: false
 
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 9dfa019f5..904daa804 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -28,6 +28,7 @@ class InitialStateSerializer < ActiveModel::Serializer
       store[:me]             = object.current_account.id.to_s
       store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal
       store[:boost_modal]    = object.current_account.user.setting_boost_modal
+      store[:favourite_modal]  = object.current_account.user.setting_favourite_modal
       store[:delete_modal]   = object.current_account.user.setting_delete_modal
       store[:auto_play_gif]  = object.current_account.user.setting_auto_play_gif
       store[:reduce_motion]  = object.current_account.user.setting_reduce_motion
diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb
index bab944c5a..19b746520 100644
--- a/app/serializers/rest/account_serializer.rb
+++ b/app/serializers/rest/account_serializer.rb
@@ -7,9 +7,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
              :note, :url, :avatar, :avatar_static, :header, :header_static,
              :followers_count, :following_count, :statuses_count
 
-  has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved?
-
-  delegate :moved?, to: :object
+  has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
 
   def id
     object.id.to_s
@@ -38,4 +36,8 @@ class REST::AccountSerializer < ActiveModel::Serializer
   def header_static
     full_asset_url(object.header_static_url)
   end
+
+  def moved_and_not_nested?
+    object.moved? && object.moved_to_account.moved_to_account_id.nil?
+  end
 end
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index a289ceac4..3be110665 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -4,7 +4,7 @@ class AccountSearchService < BaseService
   attr_reader :query, :limit, :options, :account
 
   def call(query, limit, account = nil, options = {})
-    @query   = query
+    @query   = query.strip
     @limit   = limit
     @options = options
     @account = account
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index cec96d927..d0472a1d7 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -38,7 +38,13 @@ class FetchLinkCardService < BaseService
     @card ||= PreviewCard.new(url: @url)
     res     = Request.new(:head, @url).perform
 
-    return if res.code != 200 || res.mime_type != 'text/html'
+    return if res.code != 405 && (res.code != 200 || res.mime_type != 'text/html')
+
+    @response = Request.new(:get, @url).perform
+
+    return if @response.code != 200 || @response.mime_type != 'text/html'
+
+    @html = @response.to_s
 
     attempt_oembed || attempt_opengraph
   end
@@ -70,30 +76,32 @@ class FetchLinkCardService < BaseService
   end
 
   def attempt_oembed
-    response = OEmbed::Providers.get(@url)
+    embed = OEmbed::Providers.get(@url, html: @html)
 
-    return false unless response.respond_to?(:type)
+    return false unless embed.respond_to?(:type)
 
-    @card.type          = response.type
-    @card.title         = response.respond_to?(:title)         ? response.title         : ''
-    @card.author_name   = response.respond_to?(:author_name)   ? response.author_name   : ''
-    @card.author_url    = response.respond_to?(:author_url)    ? response.author_url    : ''
-    @card.provider_name = response.respond_to?(:provider_name) ? response.provider_name : ''
-    @card.provider_url  = response.respond_to?(:provider_url)  ? response.provider_url  : ''
+    @card.type          = embed.type
+    @card.title         = embed.respond_to?(:title)         ? embed.title         : ''
+    @card.author_name   = embed.respond_to?(:author_name)   ? embed.author_name   : ''
+    @card.author_url    = embed.respond_to?(:author_url)    ? embed.author_url    : ''
+    @card.provider_name = embed.respond_to?(:provider_name) ? embed.provider_name : ''
+    @card.provider_url  = embed.respond_to?(:provider_url)  ? embed.provider_url  : ''
     @card.width         = 0
     @card.height        = 0
 
     case @card.type
     when 'link'
-      @card.image = URI.parse(response.thumbnail_url) if response.respond_to?(:thumbnail_url)
+      @card.image = URI.parse(embed.thumbnail_url) if embed.respond_to?(:thumbnail_url)
     when 'photo'
-      @card.embed_url = response.url
-      @card.width     = response.width.presence  || 0
-      @card.height    = response.height.presence || 0
+      return false unless embed.respond_to?(:url)
+      @card.embed_url = embed.url
+      @card.image     = URI.parse(embed.url)
+      @card.width     = embed.width.presence  || 0
+      @card.height    = embed.height.presence || 0
     when 'video'
-      @card.width  = response.width.presence  || 0
-      @card.height = response.height.presence || 0
-      @card.html   = Formatter.instance.sanitize(response.html, Sanitize::Config::MASTODON_OEMBED)
+      @card.width  = embed.width.presence  || 0
+      @card.height = embed.height.presence || 0
+      @card.html   = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED)
     when 'rich'
       # Most providers rely on <script> tags, which is a no-no
       return false
@@ -105,17 +113,11 @@ class FetchLinkCardService < BaseService
   end
 
   def attempt_opengraph
-    response = Request.new(:get, @url).perform
-
-    return if response.code != 200 || response.mime_type != 'text/html'
-
-    html = response.to_s
-
     detector = CharlockHolmes::EncodingDetector.new
     detector.strip_tags = true
 
-    guess = detector.detect(html, response.charset)
-    page  = Nokogiri::HTML(html, nil, guess&.fetch(:encoding, nil))
+    guess = detector.detect(@html, @response.charset)
+    page  = Nokogiri::HTML(@html, nil, guess&.fetch(:encoding, nil))
 
     if meta_property(page, 'twitter:player')
       @card.type   = :video
@@ -132,16 +134,16 @@ class FetchLinkCardService < BaseService
       @card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image')
     end
 
-    @card.title            = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || ''
-    @card.description      = meta_property(page, 'og:description').presence || meta_property(page, 'description') || ''
+    @card.title       = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || ''
+    @card.description = meta_property(page, 'og:description').presence || meta_property(page, 'description') || ''
 
     return if @card.title.blank? && @card.html.blank?
 
     @card.save_with_optional_image!
   end
 
-  def meta_property(html, property)
-    html.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || html.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value
+  def meta_property(page, property)
+    page.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || page.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value
   end
 
   def lock_options
diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb
index 9c009335b..9c3008035 100644
--- a/app/services/fetch_remote_status_service.rb
+++ b/app/services/fetch_remote_status_service.rb
@@ -40,6 +40,6 @@ class FetchRemoteStatusService < BaseService
   end
 
   def confirmed_domain?(domain, account)
-    account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url || account.uri).normalized_host).zero?
+    account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero?
   end
 end
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 20579ca63..ac0207a0a 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -22,7 +22,7 @@ class FollowService < BaseService
     elsif source_account.requested?(target_account)
       # This isn't managed by a method in AccountInteractions, so we modify it
       # ourselves if necessary.
-      req = follow_requests.find_by(target_account: other_account)
+      req = source_account.follow_requests.find_by(target_account: target_account)
       req.update!(show_reblogs: reblogs)
       return
     end
diff --git a/app/services/resolve_remote_account_service.rb b/app/services/resolve_remote_account_service.rb
index 3293fe40f..d7d0be210 100644
--- a/app/services/resolve_remote_account_service.rb
+++ b/app/services/resolve_remote_account_service.rb
@@ -44,7 +44,7 @@ class ResolveRemoteAccountService < BaseService
       if lock.acquired?
         @account = Account.find_remote(@username, @domain)
 
-        if activitypub_ready?
+        if activitypub_ready? || @account&.activitypub?
           handle_activitypub
         else
           handle_ostatus
diff --git a/app/validators/status_pin_validator.rb b/app/validators/status_pin_validator.rb
index 9760e1138..64da04120 100644
--- a/app/validators/status_pin_validator.rb
+++ b/app/validators/status_pin_validator.rb
@@ -2,9 +2,9 @@
 
 class StatusPinValidator < ActiveModel::Validator
   def validate(pin)
-    pin.errors.add(:status, I18n.t('statuses.pin_errors.reblog')) if pin.status.reblog?
-    pin.errors.add(:status, I18n.t('statuses.pin_errors.ownership')) if pin.account_id != pin.status.account_id
-    pin.errors.add(:status, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted).include?(pin.status.visibility)
-    pin.errors.add(:status, I18n.t('statuses.pin_errors.limit')) if pin.account.status_pins.count > 4
+    pin.errors.add(:base, I18n.t('statuses.pin_errors.reblog')) if pin.status.reblog?
+    pin.errors.add(:base, I18n.t('statuses.pin_errors.ownership')) if pin.account_id != pin.status.account_id
+    pin.errors.add(:base, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted).include?(pin.status.visibility)
+    pin.errors.add(:base, I18n.t('statuses.pin_errors.limit')) if pin.account.status_pins.count > 4
   end
 end
diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml
index 5265d77f6..598f6cddd 100644
--- a/app/views/admin/accounts/_account.html.haml
+++ b/app/views/admin/accounts/_account.html.haml
@@ -4,22 +4,11 @@
   %td.domain
     - unless account.local?
       = link_to account.domain, admin_accounts_path(by_domain: account.domain)
-  %td.protocol
-    - unless account.local?
-      = account.protocol.humanize
-  %td.confirmed
-    - if account.local?
-      - if account.user_confirmed?
-        %i.fa.fa-check
-      - else
-        %i.fa.fa-times
-  %td.subscribed
+  %td
     - if account.local?
-      = t('admin.accounts.location.local')
-    - elsif account.subscribed?
-      %i.fa.fa-check
+      = t("admin.accounts.roles.#{account.user&.role}")
     - else
-      %i.fa.fa-times
+      = account.protocol.humanize
   %td
     = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}")
     = table_link_to 'globe', t('admin.accounts.public'), TagManager.instance.url_for(account)
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 27a0682d8..6aa39a80a 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -31,6 +31,11 @@
         - else
           = filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1'
   .filter-subset
+    %strong= t('admin.accounts.role')
+    %ul
+      %li= filter_link_to t('admin.accounts.moderation.all'), staff: nil
+      %li= filter_link_to t('admin.accounts.roles.staff'), staff: '1'
+  .filter-subset
     %strong= t('admin.accounts.order.title')
     %ul
       %li= filter_link_to t('admin.accounts.order.alphabetic'), recent: nil
@@ -56,9 +61,7 @@
       %tr
         %th= t('admin.accounts.username')
         %th= t('admin.accounts.domain')
-        %th= t('admin.accounts.protocol')
-        %th= t('admin.accounts.confirmed')
-        %th= fa_icon 'paper-plane-o'
+        %th
         %th
     %tbody
       = render @accounts
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index ddb1cf15d..5f5d0995c 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -104,7 +104,7 @@
     - else
       = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:suspend, @account)
 
-- unless @account.local?
+- if !@account.local? && @account.hub_url.present?
   %hr
   %h3 OStatus
 
@@ -132,6 +132,7 @@
       - if @account.subscribed?
         = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' if can?(:unsubscribe, @account)
 
+- if !@account.local? && @account.inbox_url.present?
   %hr
   %h3 ActivityPub
 
diff --git a/app/views/admin/custom_emojis/_custom_emoji.html.haml b/app/views/admin/custom_emojis/_custom_emoji.html.haml
index bab34bc8d..f7fd2538c 100644
--- a/app/views/admin/custom_emojis/_custom_emoji.html.haml
+++ b/app/views/admin/custom_emojis/_custom_emoji.html.haml
@@ -7,7 +7,7 @@
     - if custom_emoji.local?
       = t('admin.accounts.location.local')
     - else
-      = custom_emoji.domain
+      = link_to custom_emoji.domain, admin_custom_emojis_path(by_domain: custom_emoji.domain)
   %td
     - if custom_emoji.local?
       - if custom_emoji.visible_in_picker
diff --git a/app/views/admin/custom_emojis/index.html.haml b/app/views/admin/custom_emojis/index.html.haml
index 20ffb8529..89ea3a6fe 100644
--- a/app/views/admin/custom_emojis/index.html.haml
+++ b/app/views/admin/custom_emojis/index.html.haml
@@ -17,6 +17,20 @@
         - else
           = filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil
 
+= form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do
+  .fields-group
+    - Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key|
+      - if params[key].present?
+        = hidden_field_tag key, params[key]
+
+    - %i(shortcode by_domain).each do |key|
+      .input.string.optional
+        = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.custom_emojis.#{key}")
+
+    .actions
+      %button= t('admin.accounts.search')
+      = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
+
 .table-wrapper
   %table.table
     %thead
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 99ae7d90d..20603678b 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -19,7 +19,11 @@
       = title
 
     = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
-    = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
+    - if @theme
+      - if @theme[:supported_locales].include? I18n.locale.to_s
+        = javascript_pack_tag "locales/#{@theme[:flavour]}/#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
+      - elsif @theme[:supported_locales].include? 'en'
+        = javascript_pack_tag "locales/#{@theme[:flavour]}/en", integrity: true, crossorigin: 'anonymous'
     = csrf_meta_tags
 
     = yield :header_tags
diff --git a/app/views/settings/flavours/show.html.haml b/app/views/settings/flavours/show.html.haml
new file mode 100644
index 000000000..43c037737
--- /dev/null
+++ b/app/views/settings/flavours/show.html.haml
@@ -0,0 +1,19 @@
+- content_for :page_title do
+  = t "flavours.#{@selected}.name", default: @selected
+
+= simple_form_for current_user, url: settings_flavour_path(@selected), html: { method: :put } do |f|
+  = render 'shared/error_messages', object: current_user
+
+  - Themes.instance.flavour(@selected)['screenshot'].each do |screen|
+    %img.flavour-screen{ src: asset_pack_path(screen) }
+
+  .flavour-description
+    = t "flavours.#{@selected}.description", default: ''
+
+  %hr/
+
+  .fields-group
+    = f.input :setting_skin, collection: Themes.instance.skins_for(@selected), label_method: lambda { |skin| I18n.t("skins.#{@selected}.#{skin}", default: skin) }, wrapper: :with_label, include_blank: false
+
+  .actions
+    = f.button :button, t('generic.use_this'), type: :submit
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index 9564c0399..d1459d93c 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -26,12 +26,9 @@
   %h4= t 'preferences.web'
 
   .fields-group
-    - if Themes.instance.flavours.size > 1
-      = f.input :setting_flavour, collection: Themes.instance.flavours, label_method: lambda { |flavour| I18n.t("themes.#{flavour}", default: flavour) }, wrapper: :with_label, include_blank: false
-      = f.input :setting_skin, collection: Themes.instance.skins_for(current_flavour), label_method: lambda { |skin| I18n.t("themes.#{current_flavour}.skins.#{skin}", default: skin) }, wrapper: :with_label, include_blank: false
-
     = f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label
     = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
+    = f.input :setting_favourite_modal, as: :boolean, wrapper: :with_label
     = f.input :setting_delete_modal, as: :boolean, wrapper: :with_label
 
   .fields-group
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index b488bd9ba..d88ec8280 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -17,16 +17,16 @@
       %p{ style: 'margin-bottom: 0' }<
         %span.p-summary> #{Formatter.instance.format_spoiler(status)}&nbsp;
         %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
-    .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
-      = Formatter.instance.format(status, custom_emojify: true)
-      - if !status.media_attachments.empty?
-        - if status.media_attachments.first.video?
-          - video = status.media_attachments.first
-          %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380) }}<
-        - else
-          %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}<
-      - elsif status.preview_cards.first
-        %div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }}<
+    .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true)
+
+  - if !status.media_attachments.empty?
+    - if status.media_attachments.first.video?
+      - video = status.media_attachments.first
+      %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380, detailed: true) }}<
+    - else
+      %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}<
+  - elsif status.preview_cards.first
+    %div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }}
 
   .detailed-status__meta
     %data.dt-published{ value: status.created_at.to_time.iso8601 }
diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml
index 895a61247..b52334a28 100644
--- a/app/views/stream_entries/show.html.haml
+++ b/app/views/stream_entries/show.html.haml
@@ -1,3 +1,6 @@
+- content_for :page_title do
+  = t('statuses.title', name: display_name(@account), quote: truncate(@stream_entry.activity.text, length: 50, omission: '…'))
+
 - content_for :header_tags do
   - if @account.user&.setting_noindex
     %meta{ name: 'robots', content: 'noindex' }/
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index e05fe1c39..03f19e20a 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -19,8 +19,11 @@
           %p= t 'about.about_hashtag_html', hashtag: @tag.name
 
           .cta
-            = link_to t('auth.login'), new_user_session_path, class: 'button button-secondary'
-            = link_to t('about.learn_more'), root_url, class: 'button button-alternative'
+            - if user_signed_in?
+              = link_to t('settings.back'), root_path, class: 'button button-secondary'
+            - else
+              = link_to t('auth.login'), new_user_session_path, class: 'button button-secondary'
+            = link_to t('about.learn_more'), about_path, class: 'button button-alternative'
 
         .features-list
           .features-list__row